-
Notifications
You must be signed in to change notification settings - Fork 15
homework-10-ataranchiev #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"]}}) | ||
|
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 с решением на ревью. | ||
|
||
|
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") | ||
(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] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Больше дестракчеринга богу дестракчеринга: |
||
(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])) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Вопрос вкуса, но имхо читаемее просто литерал мапы:
|
||
(frame-reader (drop header-size data))))) | ||
[]))) |
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)))] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Я за дестракчеринг: |
||
:when (contains? what-to-show (:id frame))] | ||
[(:title frame "None") (:body frame)])))))))) |
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])) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Я ошибаюсь, или это просто |
||
(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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Я за литерал мапы :)
|
||
(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")) |
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))) |
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))))))) |
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)))))) |
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])) |
There was a problem hiding this comment.
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 ...