Skip to content
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

ISyufu homework-10 #17

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

isyufu
Copy link

@isyufu isyufu commented Dec 25, 2023

Решил оставить решение на RandomAcsessFile. Хоть тесты с ним выглядят не очень. В следующий раз буду следовать TDD. В материалах курса не нашел mp3 файла, поэтому к тестам приложил свой. Тесты проходят и работает через lein run filename.mp3. Подправил project.clj чтобы uberjar собирался без core.clj, там есть ошибки.
GitHub почему-то пару .clj файлов не хочет воспринимать как текст, пытался через .gitattributes решить проблему, не удалось. Отображаются через "View file"

@astynax
Copy link
Contributor

astynax commented Dec 26, 2023

Лучше не класть бинарные файлы рядом с кодом. Стоит переложить в resources и подгружать с помощью clojure.java.io.resource.

margintop15px added a commit that referenced this pull request Jan 2, 2024
@zelark
Copy link
Contributor

zelark commented Jan 18, 2024

Мои комментарии

;; Ошибка в чтении размера

;; https://stackoverflow.com/questions/5223025/why-do-mp3-files-use-synchsafe-integers
(defn sync-safe-to-int [bytes]
  (bit-or
    (bit-shift-left (nth bytes 0) 21)
    (bit-shift-left (nth bytes 1) 14)
    (bit-shift-left (nth bytes 2) 7)
                    (nth bytes 3)))

;; не учитывается расширенный заголовок

;; for не для чтения байт


;; так тоже лучше не писать, часто встречается у новичков
(#(get % 3))


;; в данной ситации лучше использовать case
(defmulti read-str :byte-encoding)

;; Разделитель + финальная строка
(comment
  (str/split "abc��efg" #"\u0000{1,2}"))

;; сайд эффект в map — в Clojure так не делают
(map #(println (% :tag-name) (% :text)))

Результат лайвкодинга

(ns otus-10.homework
  (:require [clojure.java.io :as io]
            [clojure.string :as str])
  (:gen-class))

(defn sync-safe-to-int [bytes]
  (bit-or
   (bit-shift-left (nth bytes 0) 21)
   (bit-shift-left (nth bytes 1) 14)
   (bit-shift-left (nth bytes 2) 7)
   (nth bytes 3)))

(defn read-size [^java.io.RandomAccessFile raf seek]
  (let [ba (byte-array 4)]
    (.seek raf seek)
    (.read raf ba)
    (sync-safe-to-int ba)
    #_(as-> (java.nio.ByteBuffer/wrap ba) bb
        (.getInt bb))))

(defn read-header [^java.io.RandomAccessFile raf id3-seek]
  (let [id3 (let [ba (byte-array 3)]
              (.seek raf id3-seek)
              (.read raf ba)
              (String. ba))
        ver (let [ba (byte-array 2)]
              (.seek raf (+ id3-seek 3))
              (.read raf ba)
              (keyword (str "v2."  (int (first ba)))))
        size-id3 (read-size raf (+ id3-seek 6))]
    ;; FIXME: не учитывается расширенный заголовок!
    {:id3 id3 :version ver :size-id3 size-id3 :h-size 10 :id3-seek (+ 10 id3-seek) :raf raf}))

  ;; когда достигнет конца, кинет IllegalArgumentException Value out of range for char: -1
(defn find-id3-start [^java.io.RandomAccessFile raf]
  (let [iI (int \I)
        iD (int \D)
        i3 (int \3)
        lazy-3bytes-idx
        (for [i (range 0 (- (.length raf) 10))]
          (do
            (.seek raf i)
            [(.readByte raf) (.readByte raf) (.readByte raf) i]))]
    (->> lazy-3bytes-idx
         (filter (fn [[b1 b2 b3 _]]
                   (and (= iI b1) (= iD b2) (= i3 b3))))
         first
         (#(get % 3)))))

;; DEFMULTY read-str

(defmulti read-str :byte-encoding)

(defmethod read-str 0 [{ba :ba}]
  (new String ba (java.nio.charset.Charset/forName "ISO-8859-1")))

(defmethod read-str 1 [{ba :ba}]
  (new String ba (java.nio.charset.Charset/forName "UTF-16"))) ;;UTF-16LE

(defmethod read-str 2 [{ba :ba}]
  (new String ba (java.nio.charset.Charset/forName "UTF-16BE")))

(defmethod read-str 3 [{ba :ba}]
  (new String ba (java.nio.charset.Charset/forName "UTF-8")))

(defmethod read-str :default [{ba :ba}]
  (do
    (println "Unknown encoding")
    ""))

(defn split-str [str {version :version}]
  (let [splitter (case version
                   :v2.3 #"/"
                   :v2.4 #"\u0000{1,2}"
                   nil)]
    (str/join "\n" (str/split str splitter))))

;; DEFMULTY read-frame

(defmulti read-frame
  "TALB — альбом
   TIT2 — название трека
   TYER — год выхода альбома
   TCON — жанр"
  :tag)

(defmethod read-frame :TALB [m]
  {:tag-name "Альбом:" :text (split-str (read-str m) m)})

(defmethod read-frame :TIT2 [m]
  {:tag-name  "Название трека:" :text (split-str (read-str m) m)})

(defmethod read-frame :TYER [m]
  {:tag-name  "Год выхода альбома:" :text (split-str (read-str m) m)})

(defmethod read-frame :TCON [m]
  {:tag-name  "Жанр:" :text (split-str (read-str m) m)})

(defmethod read-frame :default [m]
  {:tag-name (str (m :tag)) :skipped true})

(defn read-frames [header]
  (let [^java.io.RandomAccessFile raf (header :raf)
        id3-seek (header :id3-seek)
        size-id3 (header :size-id3)
        version (header :version)]
    (loop [seek id3-seek
           res []]
      (if (or (>= seek (+ size-id3 id3-seek)) (zero? (read-size raf (+ seek 4))))
        res
        (let [tag (let [ba (byte-array (if (= version :v2.2) 3 4))] ;; 3 2.2 ; 4 2.3 2.4
                    (.seek raf seek)
                    (.read raf ba)
                    (keyword (String. ba)))
              size (read-size raf (+ seek 4))
              byte-encoding (do
                              (.seek raf (+ seek 10))
                              (.readByte raf))
              bytes (let [ba (byte-array (- size 1))]
                      (.seek raf (+ seek 11))
                      (.read raf ba)
                      ba)
              rec {:tag tag :byte-encoding byte-encoding :ba bytes :size size :version version}]
          (recur (+ seek 10 size) (conj res rec)))))))

(defn mp3-tags [path]
  (let [raf  (java.io.RandomAccessFile. path  "r")
        id3-seek (find-id3-start raf)
        header (read-header raf id3-seek)
        #_#__      (prn :header header)
        frames (read-frames header)
        #_#__      (prn (mapv :size frames))]
    (map read-frame frames)))


(defn -main [path & args]
  (println "Tags of:" path)
  (doseq [tag (mp3-tags path)
          :when (not (:skipped tag))]
    (println (tag :tag-name) (tag :text))))

(comment
  (mp3-tags "test/otus_10/1-second-of-silence.mp3")
  (mp3-tags "test/otus_10/file-12926-ed090b.mp3"))

Полезные ссылки
https://github.com/tilo/ID3/blob/master/docs/ID3-Standards/id3v2.4.0-structure.txt
https://stackoverflow.com/questions/5223025/why-do-mp3-files-use-synchsafe-integers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants