From 54a4be29ea5a43bb893718dc4b0e55572c60747c Mon Sep 17 00:00:00 2001 From: Hellow Date: Fri, 13 Jan 2023 14:37:15 +0100 Subject: [PATCH] fixed readme + id3 timestamps --- README.md | 6 ++ src/goof.py | 10 +- src/music_kraken/database/new_database.py | 22 ++--- src/music_kraken/database/objects/metadata.py | 92 +++++++++++++++++- src/music_kraken/database/objects/song.py | 43 ++++---- src/music_kraken/static_files/new_db.sql | 3 +- src/test.db | Bin 69632 -> 69632 bytes 7 files changed, 141 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 9819c4d..2f4f689 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,11 @@ pip install music-kraken music-kraken ``` +### Notes for Python 3.9 + +Unfortunately I use features that newly git introduced in [Python 3.10](https://docs.python.org/3/library/types.html#types.UnionType). +So unfortunately you **CAN'T** run this programm with python 3.9. [#10][i10] + ### Notes for WSL If you choose to run it in WSL, make sure ` ~/.local/bin` is added to your `$PATH` [#2][i2] @@ -331,5 +336,6 @@ To get the Lyrics, I scrape them, and put those in the USLT ID3 Tags of for exam For the lyrics source the page [https://genius.com/](https://genius.com/) is easily sufficient. It has most songs. Some songs are not present though, but that is fine, because the lyrics are optional anyways. +[i10]: https://github.com/HeIIow2/music-downloader/issues/10 [i2]: https://github.com/HeIIow2/music-downloader/issues/2 [mb]: https://musicbrainz.org/ diff --git a/src/goof.py b/src/goof.py index b76b06e..49de36c 100644 --- a/src/goof.py +++ b/src/goof.py @@ -16,6 +16,9 @@ from music_kraken.tagging import ( import music_kraken.database.new_database as db +import datetime +import pycountry + def div(msg: str = ""): print("-" * 50 + msg + "-" * 50) @@ -39,7 +42,10 @@ feature_artist = Artist( ) album_input = Album( - title="One Final Action" + title="One Final Action", + date=datetime.date(1986, 3, 1), + language=pycountry.languages.get(alpha_2="en"), + label="cum productions" ) album_input.artists = [ main_artist, @@ -62,7 +68,7 @@ song_input = Song( ], album=album_input, main_artist_list=[main_artist], - feature_artist_list=[feature_artist] + feature_artist_list=[feature_artist], ) other_song = Song( diff --git a/src/music_kraken/database/new_database.py b/src/music_kraken/database/new_database.py index bc9a14a..7df0cce 100644 --- a/src/music_kraken/database/new_database.py +++ b/src/music_kraken/database/new_database.py @@ -3,6 +3,8 @@ import os import logging from typing import List, Tuple from pkg_resources import resource_string +import datetime +import pycountry from .objects.parents import Reference from .objects.source import Source @@ -40,12 +42,12 @@ FROM Lyrics WHERE {where}; """ ALBUM_QUERY_UNJOINED = """ -SELECT Album.id AS album_id, title, copyright, album_status, language, year, date, country, barcode, albumsort, is_split +SELECT Album.id AS album_id, title, label, album_status, language, date, country, barcode, albumsort, is_split FROM Album WHERE {where}; """ ALBUM_QUERY_JOINED = """ -SELECT a.id AS album_id, a.title, a.copyright, a.album_status, a.language, a.year, a.date, a.country, a.barcode, a.albumsort, a.is_split +SELECT a.id AS album_id, a.title, a.label, a.album_status, a.language, a.date, a.country, a.barcode, a.albumsort, a.is_split FROM Song INNER JOIN Album a ON Song.album_id=a.id WHERE {where}; @@ -131,16 +133,15 @@ class Database: def push_album(self, album: Album): table = "Album" - query = f"INSERT OR REPLACE INTO {table} (id, title, copyright, album_status, language, year, date, country, barcode, albumsort, is_split) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);" + query = f"INSERT OR REPLACE INTO {table} (id, title, label, album_status, language, date, country, barcode, albumsort, is_split) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);" values = ( album.id, album.title, - album.copyright, + album.label, album.album_status, - album.language, - album.year, - album.date, + album.iso_639_2_language, + album.date.strftime("%Y-%m-%d"), album.country, album.barcode, album.albumsort, @@ -541,11 +542,10 @@ class Database: album_obj = Album( id_=album_id, title=album_result['title'], - copyright_=album_result['copyright'], + label=album_result['label'], album_status=album_result['album_status'], - language=album_result['language'], - year=album_result['year'], - date=album_result['date'], + language=pycountry.languages.get(alpha_3=album_result['language']), + date=datetime.datetime.strptime(album_result['date'], "%Y-%m-%d").date(), country=album_result['country'], barcode=album_result['barcode'], is_split=album_result['is_split'], diff --git a/src/music_kraken/database/objects/metadata.py b/src/music_kraken/database/objects/metadata.py index 7e0106f..859f096 100644 --- a/src/music_kraken/database/objects/metadata.py +++ b/src/music_kraken/database/objects/metadata.py @@ -1,6 +1,7 @@ from enum import Enum from typing import List, Dict, Tuple from mutagen import id3 +import datetime from .parents import ( ID3Metadata @@ -10,6 +11,9 @@ from .parents import ( class Mapping(Enum): """ These frames belong to the id3 standart + https://web.archive.org/web/20220830091059/https://id3.org/id3v2.4.0-frames + https://id3lib.sourceforge.net/id3/id3v2com-00.html + https://mutagen-specs.readthedocs.io/en/latest/id3/id3v2.4.0-frames.html """ # Textframes TITLE = "TIT2" @@ -37,7 +41,7 @@ class Mapping(Enum): LYRICIST = "TEXT" WRITER = "TEXT" ARTIST = "TPE1" - LANGUAGE = "TLAN" + LANGUAGE = "TLAN" # https://en.wikipedia.org/wiki/ISO_639-2 ITUNESCOMPILATION = "TCMP" REMIXED_BY = "TPE4" RADIO_STATION_OWNER = "TRSO" @@ -58,6 +62,7 @@ class Mapping(Enum): ALBUM = "TALB" ALBUMSORTORDER = "TSOA" ALBUMARTISTSORTORDER = "TSO2" + TAGGING_TIME = "TDTG" SOURCE_WEBPAGE_URL = "WOAS" FILE_WEBPAGE_URL = "WOAF" @@ -93,6 +98,91 @@ class Mapping(Enum): return cls.get_url_instance(key, value) +class ID3Timestamp(datetime.datetime): + def __init__( + self, + year: int = None, + month: int = None, + day: int = None, + hour: int = None, + minute: int = None, + second: int = None, + microsecond=0, + tzinfo=None, + *, + fold=0 + ): + self.has_year = year is not None + self.has_month = month is not None + self.has_day = day is not None + self.has_hour = hour is not None + self.has_minute = minute is not None + self.has_second = second is not None + self.has_microsecond = microsecond is not None + + if not self.has_year: + year = 1 + if not self.has_month: + month = 1 + if not self.has_day: + day = 1 + super().__init__( + year=year, + month=month, + day=day, + hour=hour, + minute=minute, + second=second, + microsecond=microsecond, + tzinfo=tzinfo, + fold=fold + ) + + def get_timestamp(self) -> str: + """ + https://mutagen-specs.readthedocs.io/en/latest/id3/id3v2.4.0-structure.html + + The timestamp fields are based on a subset of ISO 8601. When being as precise as possible the format of a + time string is + - yyyy-MM-ddTHH:mm:ss + - (year[%Y], “-”, month[%m], “-”, day[%d], “T”, hour (out of 24)[%H], ”:”, minutes[%M], ”:”, seconds[%S]) + - %Y-%m-%dT%H:%M:%S + but the precision may be reduced by removing as many time indicators as wanted. Hence valid timestamps are + - yyyy + - yyyy-MM + - yyyy-MM-dd + - yyyy-MM-ddTHH + - yyyy-MM-ddTHH:mm + - yyyy-MM-ddTHH:mm:ss + All time stamps are UTC. For durations, use the slash character as described in 8601, + and for multiple non-contiguous dates, use multiple strings, if allowed by the frame definition. + + :return timestamp: as timestamp in the format of the id3 time as above described + """ + + if self.has_year and self.has_month and self.has_day and self.has_hour and self.has_minute and self.has_second: + return self.strftime("%Y-%m-%dT%H:%M:%S") + if self.has_year and self.has_month and self.has_day and self.has_hour and self.has_minute: + return self.strftime("%Y-%m-%dT%H:%M") + if self.has_year and self.has_month and self.has_day and self.has_hour: + return self.strftime("%Y-%m-%dT%H") + if self.has_year and self.has_month and self.has_day: + return self.strftime("%Y-%m-%d") + if self.has_year and self.has_month: + return self.strftime("%Y-%m") + if self.has_year: + return self.strftime("%Y") + return "" + + def __str__(self) -> str: + return self.timestamp + + def __repr__(self) -> str: + return self.timestamp + + timestamp: str = property(fget=get_timestamp) + + class Metadata: """ Shall only be read or edited via the Song object. diff --git a/src/music_kraken/database/objects/song.py b/src/music_kraken/database/objects/song.py index 1b7ccc1..bb278f9 100644 --- a/src/music_kraken/database/objects/song.py +++ b/src/music_kraken/database/objects/song.py @@ -1,6 +1,7 @@ import os from typing import List, Tuple, Dict -from mutagen.easyid3 import EasyID3 +import datetime +import pycountry from .metadata import ( Mapping as ID3_MAPPING, @@ -278,11 +279,10 @@ class Album(DatabaseObject, ID3Metadata): self, id_: str = None, title: str = None, - copyright_: str = None, + label: str = None, album_status: str = None, - language: str = None, - year: str = None, - date: str = None, + language: pycountry.Languages = None, + date: datetime.date = None, country: str = None, barcode: str = None, is_split: bool = False, @@ -291,20 +291,10 @@ class Album(DatabaseObject, ID3Metadata): ) -> None: DatabaseObject.__init__(self, id_=id_, dynamic=dynamic) self.title: str = title - self.copyright: str = copyright_ self.album_status: str = album_status - """ - TODO - MAKE SURE THIS IS IN THE CORRECT FORMAT - """ - self.language: str = language - """ - TODO - only store the date in a python date object and derive the - year from it - """ - self.year: str = year - self.date: str = date + self.label = label + self.language: pycountry.Languages = language + self.date: datetime.date = date self.country: str = country """ TODO @@ -344,10 +334,25 @@ class Album(DatabaseObject, ID3Metadata): return { ID3_MAPPING.ALBUM: [self.title], ID3_MAPPING.COPYRIGHT: [self.copyright], - ID3_MAPPING.LANGUAGE: [self.language], + ID3_MAPPING.LANGUAGE: [self.iso_639_2_language], ID3_MAPPING.ALBUM_ARTIST: [a.name for a in self.artists] } + def get_copyright(self) -> str: + if self.date.year == 1 or self.label is None: + return None + + return f"{self.date.year} {self.label}" + + def get_iso_639_2_lang(self) -> str: + if self.language is None: + return None + + return self.language.alpha_3 + + copyright = property(fget=get_copyright) + iso_639_2_language = property(fget=get_iso_639_2_lang) + """ diff --git a/src/music_kraken/static_files/new_db.sql b/src/music_kraken/static_files/new_db.sql index 0be9482..ef77458 100644 --- a/src/music_kraken/static_files/new_db.sql +++ b/src/music_kraken/static_files/new_db.sql @@ -26,10 +26,9 @@ CREATE TABLE Album ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title TEXT, - copyright TEXT, + label TEXT, album_status TEXT, language TEXT, - year TEXT, date TEXT, country TEXT, barcode TEXT, diff --git a/src/test.db b/src/test.db index eb96257c37eace5acde4da55f5a8c88b3d3aeda7..34facad66eae847b994d82c28a8b00d2de18594b 100644 GIT binary patch delta 2471 zcmd6oJ#QRU6ozMaJhseU#Qn5QmbJyM zB?QHo0vCt^w4$IhG6fXK!i%UQegJ7GT%_O^M9@)kb_>8#EN)3L#mv1r^PY3w=bVG1 znS-O5&)>cJC|UkGf3TD;58H#qvxUv}M*Hd3?S<#(zimA;_u1_C*`x8wC6kSR%?`7c z$>vs9R~w&P8`<_pSx+0^8{x&LMt`>1cw_PSiEbGR8iWyudS(zKjv{TDMQB8@v6jAc6{lNF$Gj8|@ckk>cTsu7cHI=L8=Z1Z|rwf<8#$X)_9&2d&-&aXK`r0^I~!8Br2 zXrzS{NJv7QW=wc5N3X13e?2DbKGg2n&G}YU@PGv_JR(*|#Jn&_g&+{;SW_V=Ct8nw zT7R({6ZQ1wkNKMsGN;+c{CSdnBR@{^W9Y+I`P*+b+7(($o_qnm&&M?LwZ89c?DTc7 zbIT5QcKde!z0ThIyKaB_Ht_u|te}PkMFl>6=V9KB33|Hs^Kz}R<;-(|0>*S=k1nzm!-yJl%2d>Gi9{!aDu^uh}pnl zY)OqMH&Q58cwU&H9}ImzraimR_PZ+;$1oyjDZ$Sg2B#1KMzV!hBe=K0ae>)z+Yfwa zbpPbJII0gm&Fh$~rd^K7YTD%(r4#*_fMTyo z4~3HfRurX(cN?s%kza+PV7I7c}5b6T(I9wsYYwiq@EjdmI4l+=_=)8I$Yj@AvXLX2!>uXS0!3 SRZOt+$}T_FT7&l$Hd;E{{<8(zbBkZM8VkGgRr2WW?RS&u@5z4B`XX6a zU0sd7`mp+P>GPHu?Ofj-Y;KLKsPp#1b*FZFyjiWo+mF~DI)0Hu}y zg);vT5=rM2=0a7=Cr_1xAjc(CKy!bZVu69c%mHo`g%}$Iv+R7YFxLjd&EET?;ilpm zOUgAM0z*JGML^I30>~MJ5DTk}_$*q;*Vmd+4lR__WCkc^6rhFCKv-S?q_9RNLn?Mu zRd-LW;Skt%qtS@d{YLsYJxuqfD;uAt<(W8-Ld3@P!4R>{({bMVPu`yPvS-trCh$c1 zDuYNGhyYgJIVutMJ{ty*E}%BT8Utl@ZvFIoA+Vo#?_R#R*vbu(m=(|gi4VK7wNz4K-~7!4;^pQQbe7biQvbW7|Eq*7|2tSbB%TJAj;1%T3OCL!U5$&k6Y z+)VODBhQFX5@Q4`cD30^0F8tM8CIAos3~;v-_!Hfal3M}A0@dnx#nC$nfYzke^xokJHuRm81;N zIm2@RiZrXmZnU>m6opHecsT-XZ&i=h}R4b%J#wDiqovqQ{_-r%I zS2Ka?>L-9m>r1gP1<=sDQyA-r%Oo;d?Coq1#(X}`*SOC|Bguf_+G9gS>wtuh6FFfN z<3vPi`t;#W7>%TE`v6QeUmpyi+Vp0esmty5x4p@4zjn)zatUE@DW1#o#SpY3{+GuD zaH;9xg6^*)aL1uVHM!?aE*SAXX&F-SA&Fd!;ye!lJId_DbB9a)7`^Zp DDC&i?