diff --git a/README.md b/README.md index 7bdc62e..bc2edbe 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,4 @@ 自用Python脚本库 [check.py](check.py) 校验文件用脚本 [bdshare.py](bdshare.py) 生成百度网盘标准提取码 +[convertiTunes.py](convertiTunes.py) 批量转换iTunes库音乐(未完成) diff --git a/convertiTunesSong.py b/convertiTunesSong.py new file mode 100644 index 0000000..0911f2d --- /dev/null +++ b/convertiTunesSong.py @@ -0,0 +1,578 @@ +# convertiTunesSong.py +# v0.0.1 +# (C) 2020 lifegpc +# The repo location: https://github.com/lifegpc/pythonscript +# +# This program 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. +# +# This program 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 this program. If not, see . +from xml.sax import make_parser, ContentHandler +from xml.sax.handler import feature_namespaces +from base64 import decodebytes +from time import strptime, strftime, localtime +from os.path import exists, splitext, basename, dirname +from regex import search +from urllib.parse import unquote_plus + + +class InvalidiTunesXMLException(Exception): + def __init__(self, s=None): + if s is None: + Exception.__init__("Invalid iTunes XML.") + else: + Exception.__init__(f"Invalid iTunes XML: {s}") + + +class iTunesContentHandler(ContentHandler): + data = None + dictlist = [] + i = -1 + key = "" + tempc = "" + dic = None + hasd = False + dit = 0 + + def startElement(self, tag, attributes): + if tag == "dict": + if self.i == -1: + self.data = {} + self.dic = self.data + self.dictlist.append(self.data) + else: + if isinstance(self.dic, dict): + self.dic[self.key] = {} + self.dic = self.dic[self.key] + else: + t = {} + self.dic.append(t) + self.dic = t + self.dictlist.append(self.dic) + self.i = self.i + 1 + elif tag == "array": + if self.i == -1: + raise InvalidiTunesXMLException() + else: + if isinstance(self.dic, dict): + self.dic[self.key] = [] + self.dic = self.dic[self.key] + else: + t = [] + self.dic.append(t) + self.dic = t + self.dictlist.append(self.dic) + self.i = self.i + 1 + elif tag in ['plist', 'key', 'string', 'data', 'date', 'true', 'false', 'real', 'integer']: + pass + else: + raise InvalidiTunesXMLException(f"Unknown tag: {tag}") + if tag != "plist" and tag != "dict" and tag != "array": + self.hasd = True + + def characters(self, context): + if self.hasd: + self.tempc = self.tempc + context + + def endElement(self, tag): + if tag == "dict" or tag == "array": + self.i = self.i - 1 + if self.i != -1: + self.dictlist.remove(self.dic) + self.dic = self.dictlist[self.i] + elif tag == 'key': + self.key = self.tempc + elif tag == 'string': + if isinstance(self.dic, dict): + self.dic[self.key] = self.tempc + else: + self.dic.append(self.tempc) + elif tag == "data": + b = decodebytes(self.tempc.encode()) + if isinstance(self.dic, dict): + self.dic[self.key] = b + else: + self.dic.append(b) + elif tag == 'date': + t = strptime(self.tempc, '%Y-%m-%dT%H:%M:%SZ') + if isinstance(self.dic, dict): + self.dic[self.key] = t + else: + self.dic.append(t) + elif tag == "true": + if isinstance(self.dic, dict): + self.dic[self.key] = True + else: + self.dic.append(True) + elif tag == "false": + if isinstance(self.dic, dict): + self.dic[self.key] = False + else: + self.dic.append(False) + elif tag == 'real': + f = float(self.tempc) + if isinstance(self.dic, dict): + self.dic[self.key] = f + else: + self.dic.append(f) + elif tag == "integer": + i = int(self.tempc) + if isinstance(self.dic, dict): + self.dic[self.key] = i + else: + self.dic.append(i) + self.tempc = "" + self.hasd = False + + +def ParserXML(fn: str): + p = make_parser() + p.setFeature(feature_namespaces, 0) + h = iTunesContentHandler() + p.setContentHandler(h) + p.parse(fn) + data = p.getContentHandler().data + return data + + +class main(): + fn = '' # The path of iTunes Library.xml + sall = True # select all? + fmt = '' # output file format + ext = 'm4a' # output flie ext + fnt = '/.' # File name template + ab = "320k" # bitrate + tft = '%Y-%m-%d %H:%M:%S' # Time format template + + def __init__(self, ip: dict = {}): + if 'fn' in ip: + self.fn = ip['fn'] + else: + self.fn = 'iTunes Library.xml' + if not exists(self.fn): + raise FileNotFoundError(self.fn) + data = ParserXML(self.fn) + if self.sall: + if 'Tracks' in data: + l: dict = self._filterList(data['Tracks']) + for key in l.keys(): + d = l[key] + fn = self._getFileName(d) + print(fn) + + def _filterList(self, li: dict): + l = {} + for key in li.keys(): + d = li[key] + if (not 'Protected' in d or not d['Protected']) and d['Track Type'] == "File": + l[key] = d + return l + + def _getFileName(self, data: dict): + un = 'Unknown' + reg = r'[\\]?<([^>]*)>' + reg4 = r'[^[:print:]/\\:\*\?\"<>\|]' + fn = self.fnt + ns = search(reg, fn) + while ns is not None: + ma = ns.group() + if ma[0] == '\\': + pos = ns.end() + ns = search(reg, fn, pos=pos) + continue + le = 0 + for i in range(len(ma)): + if i > 0 and ma[i-1] == '\\': + continue + if ma[i] == '<': + le = le + 1 + if ma[i] == '>': + le = le - 1 + pos = ns.end() + while le > 0: + ma = ma + fn[pos] + pos = pos + 1 + i = i + 1 + if ma[-2] == '\\': + continue + if ma[i] == '<': + le = le + 1 + if ma[i] == '>': + le = le - 1 + if ma[-2] == '\\': + pos = ns.end() + ns = search(reg, fn, pos=pos) + continue + ke = ma[1:-1] + unf = True + if ke[0] == '?': + unf = False + if len(ke) > 1: + ke = ke[1:] + else: + pos = ns.end() + ns = search(reg, fn, pos=pos) + continue + at = "" + at3 = "" + if ke[0] == '(': + at = "(" + le = 1 + i = 1 + while le > 0: + at = at + ke[i] + i = i + 1 + if at[-2] == '\\': + continue + if at[-1] == '(': + le = le + 1 + if at[-1] == ')': + le = le - 1 + ke = ke.replace(at, '', 1) + at = at[1:-1] + at, at3 = self._splitat(at) + at2 = "" + at4 = "" + if len(ke) > 1 and ke[-1] == ')' and ke[-2] != '\\': + at2 = ")" + le = 1 + i = -2 + while le > 0: + at2 = ke[i] + at2 + i = i - 1 + if ke[i] == '\\': + continue + if at2[0] == '(': + le = le - 1 + if at2[0] == ')': + le = le + 1 + ke = ke.replace(at2, '', 1) + at2 = at2[1:-1] + at2, at4 = self._splitat(at2) + pos = ns.start() + if ke == "ext": + fn = fn.replace(ma, self.ext, 1) + elif ke == 'Album': + if 'Album' in data: + fn = fn.replace(ma, f"{at}{data['Album']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "AlbumArtist": + if 'Album Artist' in data: + fn = fn.replace(ma, f"{at}{data['Album Artist']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == 'Artist': + if 'Artist' in data: + fn = fn.replace(ma, f"{at}{data['Artist']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "ArtworkCount": + if 'Artwork Count' in data: + fn = fn.replace(ma, f"{at}{data['Artwork Count']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "BPM": + if 'BPM' in data: + fn = fn.replace(ma, f"{at}{data['BPM']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "BitRate": + if 'Bit Rate' in data: + fn = fn.replace(ma, f"{at}{data['Bit Rate']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "Compilation": + if 'Compilation' in data: + fn = fn.replace(ma, f"{at}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "Composer": + if 'Composer' in data: + fn = fn.replace(ma, f"{at}{data['Composer']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "DateAdded": + if 'Date Added' in data: + fn = fn.replace( + ma, f"{at}{strftime(self.tft, data['Date Added'])}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "DateModified": + if 'Date Modified' in data: + fn = fn.replace( + ma, f"{at}{strftime(self.tft, data['Date Modified'])}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "DiscCount": + if 'Disc Count' in data: + fn = fn.replace(ma, f"{at}{data['Disc Count']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "DiscNumber": + if 'Disc Number' in data: + fn = fn.replace(ma, f"{at}{data['Disc Number']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "FileFolderCount": + if 'File Folder Count' in data: + fn = fn.replace( + ma, f"{at}{data['File Folder Count']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "FileName": + if 'Location' in data: + fn = fn.replace( + ma, f"{at}{unquote_plus(splitext(basename(data['Location']))[0])}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "Genre": + if 'Genre' in data: + fn = fn.replace(ma, f"{at}{data['Genre']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "Kind": + if 'Kind' in data: + fn = fn.replace(ma, f"{at}{data['Kind']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "LibraryFolderCount": + if 'Library Folder Count' in data: + fn = fn.replace( + ma, f"{at}{data['Library Folder Count']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "Location": + if 'Location' in data: + loc = unquote_plus(dirname(data['Location'])) + if loc.startswith('file://localhost/'): + loc = loc[17:] + fn = fn.replace(ma, f"{at}{loc}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "Name": + if 'Name' in data: + fn = fn.replace(ma, f"{at}{data['Name']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "PersistentID": + if 'Persistent ID' in data: + fn = fn.replace(ma, f"{at}{data['Persistent ID']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "PlayCount": + if 'Play Count' in data: + fn = fn.replace(ma, f"{at}{data['Play Count']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "PlayDate": + if 'Play Date' in data: + fn = fn.replace( + ma, f"{at}{strftime(self.tft, localtime(data['Play Date']))}{at2}", 1) + elif 'Play Date UTC' in data: + fn = fn.replace( + ma, f"{at}{strftime(self.tft, data['Play Date UTC'])}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "Purchased": + if 'Purchased' in data and data['Purchased']: + fn = fn.replace(ma, f"{at}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "ReleaseDate": + if 'Release Date' in data: + fn = fn.replace( + ma, f"{at}{strftime(self.tft, data['Release Date'])}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "SampleRate": + if 'Sample Rate' in data: + fn = fn.replace(ma, f"{at}{data['Sample Rate']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "Size": + if 'Size' in data: + fn = fn.replace(ma, f"{at}{data['Size']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "SkipCount": + if 'Skip Count' in data: + fn = fn.replace(ma, f"{at}{data['Skip Count']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "SkipDate": + if 'Skip Date' in data: + fn = fn.replace( + ma, f"{at}{strftime(self.tft, data['Skip Date'])}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "SortAlbum": + if 'Sort Album' in data: + fn = fn.replace(ma, f"{at}{data['Sort Album']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "SortArtist": + if 'Sort Artist' in data: + fn = fn.replace(ma, f"{at}{data['Sort Artist']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "SortComposer": + if 'Sort Composer' in data: + fn = fn.replace(ma, f"{at}{data['Sort Composer']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "SortName": + if 'Sort Name' in data: + fn = fn.replace(ma, f"{at}{data['Sort Name']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "TotalTime": + if 'Total Time' in data: + fn = fn.replace(ma, f"{at}{data['Total Time']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "TrackCount": + if 'Track Count' in data: + fn = fn.replace(ma, f"{at}{data['Track Count']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "TrackID": + if 'Track ID' in data: + fn = fn.replace(ma, f"{at}{data['Track ID']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "TrackNumber": + if 'Track Number' in data: + fn = fn.replace(ma, f"{at}{data['Track Number']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "TrackType": + if 'Track Type' in data: + fn = fn.replace(ma, f"{at}{data['Track Type']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + elif ke == "Year": + if 'Year' in data: + fn = fn.replace(ma, f"{at}{data['Year']}{at2}", 1) + elif unf: + fn = fn.replace(ma, f"{at3}{un}{at4}", 1) + else: + fn = fn.replace(ma, f'{at3}{at4}', 1) + else: + pos = ns.end() + ns = search(reg, fn, pos=pos) + fn = fn.replace('\\<', '<') + fn = fn.replace('\\>', '>') + fn = fn.replace('\\(', '(') + fn = fn.replace('\\)', ')') + rs = search(reg4, fn) + while rs is not None: + fn = fn.replace(rs.group(), '_') + rs = search(reg4, fn) + while len(fn) > 0 and fn[0] == ' ': + fn = fn[1:] + return fn + + def _splitat(self, s: str): + atl = s.split('|') + asm = [atl[0]] + j = 0 + for i in range(1, len(atl)): + if atl[i-1][-1] == '\\' or j == 1: + if atl[i-1][-1] == '\\': + asm[j] = asm[j][:-1] + '|' + atl[i] + else: + asm[j] = asm[j] + atl[i] + else: + j = j + 1 + asm.append(atl[i]) + if len(asm) == 1: + return asm[0], '' + else: + return asm[0], asm[1] + + +if __name__ == "__main__": + main()