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()