Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions otus-10/project.clj
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
(defproject otus-10 "0.1.0-SNAPSHOT"
:description "https://github.com/Clojure-Developer/Clojure-Developer-2023-10"
:dependencies [[org.clojure/clojure "1.11.1"]]
:dependencies [[org.clojure/clojure "1.11.1"]
[org.clojure/tools.cli "1.1.230"]]
:repl-options {:init-ns otus-10.core}
:main ^:skip-aot otus-10.homework
:target-path "target/%s"
:profiles {:dev {}
:uberjar {:aot :all
:profiles {:dev {:aot [otus-10.homework]},
:uberjar {:aot [otus-10.homework],
:jvm-opts ["-Dclojure.compiler.direct-linking=true"]}})

Binary file added otus-10/resources/file-12926-ed090b.mp3
Binary file not shown.
Binary file added otus-10/resources/sample.mp3
Binary file not shown.
38 changes: 38 additions & 0 deletions otus-10/src/otus_10/README-homework.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Домашнее задание

Получение информации из ID3v2-тегов в mp3-файлах
Цель:

Попрактиковаться использовать полиморфизма для обобщения кода алгоритма и предоставления возможностей для расширения возможностей.

Описание/Пошаговая инструкция выполнения домашнего задания:

Реализовать поиск тега ID3v2 (v2.4), получение размера тега, чтение его фреймов.

Реализовать с помощью case декодирование текстовых данных (v2.4 поддерживает 4 кодировки)

Реализовать в виде мультиметода декодирование фреймов:

TALB — Album
TPE1 — Artist
TIT2 — Title
TYER — Year
TCON — Genre

Вспомогательные материалы

https://mutagen-specs.readthedocs.io/en/latest/id3/id3v2.4.0-structure.html
https://mutagen-specs.readthedocs.io/en/latest/id3/id3v2.4.0-frames.html
https://clojure.org/reference/multimethods
https://clojure.org/reference/protocols
https://aphyr.com/posts/352-clojure-from-the-ground-up-polymorphism


Критерии оценки:

Написан обобщенный код для вычитывания тегов с учётом формата кодирования;
Использован полиморфизм для обработки тегов разных типов и для декодирования текста в разных представлениях;
Написаны тесты;
Отправлен PR с решением на ревью.


89 changes: 89 additions & 0 deletions otus-10/src/otus_10/frames.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
(ns otus-10.frames
(:require [otus-10.utils :as u]))

(defn decode-text
"$00 ISO-8859-1 [ISO-8859-1]. Terminated with $00.
$01 UTF-16 [UTF-16] encoded Unicode [UNICODE] with BOM. All
strings in the same frame SHALL have the same byteorder.
Terminated with $00 00.
$02 UTF-16BE [UTF-16] encoded Unicode [UNICODE] without BOM.
Terminated with $00 00.
$03 UTF-8 [UTF-8] encoded Unicode [UNICODE]. Terminated with $00."
[id text]
(case id
0 (String. (byte-array text) "ISO-8859-1")
1 (String. (byte-array text) "UTF-16")
2 (String. (byte-array text) "UTF-16BE")
3 (String. (byte-array text) "UTF-8")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно без дублирования (String. (byte-array text) (case id ...

(throw (Exception. "Unknown encoding"))))

(defn valid-frame?
"Checks first 4 symbols if they are fit frame ID pattern."
[id]
(and (string? id)
(= 4 (count id))
(some? (re-matches #"[A-Z]{3}[A-Z0-9]" id))))

(comment
(valid-frame? "asdf")
(valid-frame? "TALB")
(valid-frame? "1ASD")
(valid-frame? "1AS")
(valid-frame? "RVA2"))

(defmulti decode-frame :id)

; "TALB — Album"
(defmethod decode-frame :TALB
[data]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Больше дестракчеринга богу дестракчеринга:
[{:keys [enc-byte body] :as data}]

(assoc data :title "Album" :body (decode-text (:enc-byte data) (:body data))))

; "TPE1 — Artist"
(defmethod decode-frame :TPE1
[data]
(assoc data :title "Artist" :body (decode-text (:enc-byte data) (:body data))))

; ; "TIT2 — Title"
(defmethod decode-frame :TIT2
[data]
(assoc data :title "Title" :body (decode-text (:enc-byte data) (:body data))))

; "TYER — Year"
(defmethod decode-frame :TYER
[data]
(assoc data :title "Year" :body (Integer/parseInt (decode-text (:enc-byte data) (:body data)))))

; "TCON — Genre"
(defmethod decode-frame :TCON
[data]
(assoc data :title "Genre" :body (decode-text (:enc-byte data) (:body data))))

(defmethod decode-frame :APIC [data] (assoc data :title "Picture" :body "<Picture here>"))

;; Other
(defmethod decode-frame :TRCK
[data]
(assoc data :title "Track" :body (Integer/parseInt (apply str (:body data)))))

(defmethod decode-frame :default [data] data)

(defn frame-reader
"Returns map with frames.
:id :size :enc-byte :body
Uses this pattern to gather data:
Frame ID $xx xx xx xx (four characters)
Size 4 * %0xxxxxxx
Flags $xx xx "
([data]
(if (and (not-empty data) (valid-frame? (apply str (take 4 data))))
(let [id (take 4 data)
data-size (u/bytes->num (map int (take 4 (drop 4 data))))
all-body (take data-size (drop 10 data))
enc-byte (int (nth all-body 0))
body (map int (drop 1 all-body))
header-size (+ 10 data-size)]
(lazy-seq (cons (decode-frame (zipmap [:id :size :enc-byte :body]
[(keyword (apply str id)) header-size enc-byte body]))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вопрос вкуса, но имхо читаемее просто литерал мапы:

{:id (keyword (apply str id))
:size header-size
:enc-byte enc-byte
:body body}

(frame-reader (drop header-size data)))))
[])))
39 changes: 37 additions & 2 deletions otus-10/src/otus_10/homework.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,41 @@
(ns otus-10.homework)
(ns otus-10.homework
(:require [clojure.tools.cli :refer [parse-opts]]
[clojure.java.io :as io]
[clojure.string :as string]
[otus-10.id3 :as id3])
(:gen-class))

(def cli-options
[["-f" "--filename FILE" "Filename" :validate [#(.exists (io/as-file %))]]
["-a" "--show-album" :id :TALB]
["-A" "--show-artist" :id :TPE1]
["-t" "--show-title" :id :TIT2]
["-y" "--show-year" :id :TYER]
["-g" "--show-genre" :id :TCON]
["-h" "--help"]])

(defn usage [options-summary]
(->> ["Show mp3 info"
""
"Usage: program-name [options]"
""
"Options:"
options-summary
""
"Please refer to the manual page for more information."]
(string/join \newline)))

(defn -main
"I don't do a whole lot ... yet."
[& args])
[& args]
(let [{opts :options, errors :errors summary :summary} (parse-opts args cli-options)]
(if errors
(apply println errors)
(let [what-to-show (set (keys (dissoc opts :filename)))
frames (:frames (id3/get-id3-header (:filename opts)))]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я за тред-макросы :)

(if (empty? what-to-show)
(print (usage summary))
(dorun (map #(println (string/join " - " %))
(for [frame frames
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я за дестракчеринг:
(for [{:keys [id title body]} frames

:when (contains? what-to-show (:id frame))]
[(:title frame "None") (:body frame)]))))))))
41 changes: 41 additions & 0 deletions otus-10/src/otus_10/id3.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
(ns otus-10.id3
(:require [clojure.java.io :as io]
[otus-10.utils :as u]
[otus-10.frames :as f]))

(defn valid-id3?
"Checks if id3 is valid according to the pattern:
$49 44 33 yy yy xx zz zz zz zz"
[header]
(let [[h1 h2 h3 yy1 yy2 _ zz1 zz2 zz3 zz4] header]
(and (every? true? (map = [\I \D \3] [h1 h2 h3]))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я ошибаюсь, или это просто (= [\I \D \3] [h1 h2 h3]) == (= "ID3" (str h1 h2 h3)) ?

(every? #(< (int %) 0xff) [yy1 yy2])
(every? #(< (int %) 0x80) [zz1 zz2 zz3 zz4]))))

(defn get-id3-flags
"Returns map of base header flags."
[header]
(let [flag-bit (int (nth header 5))]
(zipmap [:unsync :extended :exp :footer]
[(bit-test flag-bit 7) (bit-test flag-bit 6) (bit-test flag-bit 5)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я за литерал мапы :)

(let [flag-bit (int (nth header 5))
     bt #(bit-test flag-bit %)]
{:unsync (bt 7)
:extended (bt 6)
....

(bit-test flag-bit 4)])))

(defn get-id3-header
"Returns map with set header flags and frames."
[filename]
(with-open [r (io/reader filename)]
(let [f (slurp r)
header (take 10 f)
size (u/bytes->num (map int (drop 6 header)))
full-header (take size f)
flags (get-id3-flags header)
ext-header-size (if (:extended flags) (u/bytes->num (map int (take 4 (drop 10 f)))) 0)
frames (drop (+ 10 ext-header-size) full-header)]
(when (valid-id3? header)
(assoc flags
:header-size size
:frames (f/frame-reader frames))))))

(comment
(get-id3-header "resources/file-12926-ed090b.mp3")
(get-id3-header "resources/sample.mp3"))
9 changes: 9 additions & 0 deletions otus-10/src/otus_10/utils.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
(ns otus-10.utils)

;; Helpers
(defn bytes->num
[data]
(reduce bit-or
(map-indexed
(fn [i x] (bit-shift-left (bit-and x 0x0FF) (* 8 (- (count data) i 1))))
data)))
29 changes: 29 additions & 0 deletions otus-10/test/otus_10/frames_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
(ns otus-10.frames-test
(:require [clojure.test :refer :all]
[otus-10.frames :refer :all]))

(deftest test-valid-frame?
(let [test-cases [{:id "ABCD" :expected true}
{:id "A2C4" :expected false}
{:id "A2C" :expected false}
{:id "ABCDE" :expected false}
{:id "A2C!" :expected false}
{:id 1234 :expected false}
{:id "" :expected false}
{:id "1234" :expected false}
{:id nil :expected false}]]
(doseq [{:keys [id expected]} test-cases]
(is (= (valid-frame? id) expected)
(str "Test failed for id: " id)))))

(deftest decode-text-test
(let [test-cases [{:id 0, :text [104 101 108 108 111], :expected "hello"}
{:id 1, :text [-2, -1, 0, 104, 0, 101, 0, 108, 0, 108, 0, 111], :expected "hello"}
{:id 2, :text [0, 104, 0, 101, 0, 108, 0, 108, 0, 111], :expected "hello"}
{:id 3, :text [104 101 108 108 111], :expected "hello"}
{:id 4, :text [104 101 108 108 111], :expected :exception}]]

(doseq [{:keys [id text expected]} test-cases]
(if (= expected :exception)
(is (thrown? Exception (decode-text id text)))
(is (= expected (decode-text id text)))))))
23 changes: 23 additions & 0 deletions otus-10/test/otus_10/id3_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
(ns otus-10.id3-test
(:require [clojure.test :refer :all]
[otus-10.id3 :refer :all]))

(deftest test-valid-id3?
(let [test-cases [{:input [\I \D \3 0x00 0x00 nil 0x00 0x00 0x00 0x00] :expected true}
{:input [\I \D \3 0x7F 0x7F nil 0x7F 0x7F 0x7F 0x7F] :expected true}
{:input [\I \D \2 0x00 0x00 nil 0x00 0x00 0x00 0x00] :expected false}
{:input [\I \D \3 0x80 0x00 nil 0x00 0x00 0x00 0x00] :expected true}
{:input [\I \D \3 0x00 0x00 nil 0x79 0x00 0x00 0x00] :expected true}
{:input [nil \D \3 0x00 0x00 nil 0x00 0x00 0x00 0x00] :expected false}]]
(doseq [{:keys [input expected]} test-cases]
(is (= expected (valid-id3? input))))))

(deftest get-id3-flags-test
(let [test-cases [{:header [0 0 0 0 0 128] :expected {:unsync true :extended false :exp false :footer false}}
{:header [0 0 0 0 0 64] :expected {:unsync false :extended true :exp false :footer false}}
{:header [0 0 0 0 0 32] :expected {:unsync false :extended false :exp true :footer false}}
{:header [0 0 0 0 0 16] :expected {:unsync false :extended false :exp false :footer true}}
{:header [0 0 0 0 0 240] :expected {:unsync true :extended true :exp true :footer true}}
{:header [0 0 0 0 0 0] :expected {:unsync false :extended false :exp false :footer false}}]]
(doseq [{:keys [header expected]} test-cases]
(is (= expected (get-id3-flags header))))))
15 changes: 15 additions & 0 deletions otus-10/test/otus_10/utils_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
(ns otus-10.utils-test
(:require [clojure.test :refer :all]
[otus-10.utils :refer :all]))

(deftest test-bytes->num
(are [expected input] (= expected (bytes->num input))
;; Test cases
0 [0]
255 [255]
256 [1 0]
65535 [255 255]
16777215 [255 255 255]
4294967295 [255 255 255 255]
1 [0 0 0 1]
258 [0 1 2]))