;;; my-spc.el -- handling spc files -*- lexical-binding: t -*- ;; Copyright (C) 2023 Free Software Foundation. ;; Author: Yuchen Pei ;; Warren Wilkinson ;; Maintainer: Yuchen Pei ;; Package-Requires: ((emacs "28")) ;; This file is part of dotted. ;; dotted is free software: you can redistribute it and/or modify it under ;; the terms of the GNU Affero General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; dotted is distributed in the hope that it will be useful, but WITHOUT ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General ;; Public License for more details. ;; You should have received a copy of the GNU Affero General Public ;; License along with dotted. If not, see . ;;; Commentary: ;; handling spc files. ;;; Code: (require 'bindat) (defconst my-spc--id666-magic-array [#x53 #x4e #x45 #x53 #x2d #x53 #x50#x43 #x37 #x30 #x30 #x20 #x53 #x6f #x75 #x6e #x64 #x20 #x46 #x69 #x6c #x65 #x20 #x44 #x61 #x74 #x61 #x20 #x76 #x30 #x2e #x33 #x30] "id666 header magic pattern `SNES-SPC700 Sound File Data v0.30'") (defconst my-spc--id666-header-bindat-spec '((file-identifier vec 33) (eval (unless (equal last my-spc--id666-magic-array) (error "id666 framing mismatch: expected `%s', got `%s'" my-spc--id666-magic-array last))) (unused u16) (has-id666 u8) (revision u8) (pc-reg u16) (a-reg u8) (x-reg u8) (y-reg u8) (psw-reg u8) (sp-reg u8) (res-reg u16) (song-title strz 32) (game-title strz 32) (dumper strz 16) (comment strz 32) (date strz 11) (fadeout vec 3) (fadeout-length vec 5) (artist strz 32)) "id666 header specification. Sources: - URL `https://ocremix.org/info/SPC_Format_Specification' - URL `https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html'") (defun my-spc--decode-id666-header (filename) "Read and decode id666 header from FILENAME." (with-temp-buffer (set-buffer-multibyte nil) (insert-file-contents-literally filename nil 0 210) (bindat-unpack my-spc--id666-header-bindat-spec (buffer-string)))) (defun my-spc-decode-id666 (filename) "Read and decode id666 metadata from FILENAME. Return metadata in a list of (FIELD . VALUE) cons cells, or nil in case of errors or if there were no known fields in FILENAME." (condition-case nil (let ((header (my-spc--decode-id666-header filename))) (when (= 26 (bindat-get-field header 'has-id666)) `( (title . ,(bindat-get-field header 'song-title)) (album . ,(bindat-get-field header 'game-title)) (artist . ,(bindat-get-field header 'artist)) (comment . ,(bindat-get-field header 'comment)) (dumper . ,(bindat-get-field header 'dumper)) (date . ,(bindat-get-field header 'date)) (fadeout . ,(bindat-get-field header 'fadeout)) (fadeout-length . ,(bindat-get-field header 'fadeout)) (extension . ,(file-name-extension filename)) (number . ,(replace-regexp-in-string "[^-]+-" "" (file-name-base filename))) ))) (error nil))) (defun my-spc-format-file-name (file) "Format a filename from an spc FILE. artist/album/title (note).extension" (when-let* ((metadata (my-spc-decode-id666 file)) (artist (alist-get 'artist metadata)) (album (alist-get 'album metadata)) (title (alist-get 'title metadata)) (extension (alist-get 'extension metadata)) (number (alist-get 'number metadata))) (if (or (string-empty-p artist) (string-empty-p album) (string-empty-p title) (string-empty-p extension) (string-empty-p number)) nil (format "%s/%s/%s %s.%s" artist album number title extension)))) (defun my-spc-file-rename (dir) "Rename all spc files under DIR using dired." (let ((pairs (seq-filter 'cdr (mapcar (lambda (file) (cons (expand-file-name file) (format "%s/%s" (expand-file-name my-audio-incoming-dir) (my-spc-format-file-name file)))) (directory-files-recursively dir "\\`.*\\.spc\\'"))))) (when (y-or-n-p (print (format "Will do the following renaming: %s\nContinue?" (string-join (mapcar (lambda (pair) (format "%s -> %s" (car pair) (cdr pair))) pairs) "\n")))) (mapc (lambda (pair) (dired-rename-file (car pair) (cdr pair) t)) pairs)))) (provide 'my-spc) ;;; my-spc.el ends here