- commit
- 5dde63bd24c25aeeb5fff7c638850eb702a4723c
- parent
- 0bbfcf656b5d71982d8807aac0de300d9cbd7b8f
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2023-02-20 06:14
replace tabs by spaces flake8 no longer supports tabs
Diffstat
| M | m3u2xspf.py | 92 | ++++++++++++++++++++++++++++++------------------------------ |
| M | xspf2m3u.py | 226 | +++++++++++++++++++++++++++++++------------------------------ |
2 files changed, 160 insertions, 158 deletions
diff --git a/m3u2xspf.py b/m3u2xspf.py
@@ -6,71 +6,71 @@ import argparse 6 6 from xml.sax.saxutils import escape 7 7 8 8 try:9 -1 import mutagen-1 9 import mutagen 10 10 except ImportError:11 -1 mutagen = None-1 11 mutagen = None 12 12 13 13 KEYS = {14 -1 'location': 'location',15 -1 'album': 'album',16 -1 'artist': 'creator',17 -1 'title': 'title',-1 14 'location': 'location', -1 15 'album': 'album', -1 16 'artist': 'creator', -1 17 'title': 'title', 18 18 } 19 19 20 20 21 21 def unlist(s):22 -1 return s if isinstance(s, str) else '; '.join(s)-1 22 return s if isinstance(s, str) else '; '.join(s) 23 23 24 24 25 25 def get_tags(path):26 -1 if not mutagen or not os.path.exists(path):27 -1 return {'location': path}28 -1 data = mutagen.File(path, easy=True)29 -1 if data:30 -1 return data31 -1 else:32 -1 return {'location': path}-1 26 if not mutagen or not os.path.exists(path): -1 27 return {'location': path} -1 28 data = mutagen.File(path, easy=True) -1 29 if data: -1 30 return data -1 31 else: -1 32 return {'location': path} 33 33 34 34 35 35 def iter_lines(path):36 -1 root = os.path.dirname(path)37 -1 with open(sys.argv[1]) as fh:38 -1 for line in fh:39 -1 line = line.rstrip()40 -1 if line.startswith('#'):41 -1 pass42 -1 elif line.startswith('http'):43 -1 yield {'location': line}44 -1 else:45 -1 if not line.startswith('/'):46 -1 line = os.path.join(root, line)47 -1 yield get_tags(line)-1 36 root = os.path.dirname(path) -1 37 with open(sys.argv[1]) as fh: -1 38 for line in fh: -1 39 line = line.rstrip() -1 40 if line.startswith('#'): -1 41 pass -1 42 elif line.startswith('http'): -1 43 yield {'location': line} -1 44 else: -1 45 if not line.startswith('/'): -1 46 line = os.path.join(root, line) -1 47 yield get_tags(line) 48 48 49 49 50 50 def parse_args():51 -1 parser = argparse.ArgumentParser()52 -1 parser.add_argument('src')53 -1 return parser.parse_args()-1 51 parser = argparse.ArgumentParser() -1 52 parser.add_argument('src') -1 53 return parser.parse_args() 54 54 55 55 56 56 def main():57 -1 args = parse_args()58 -159 -1 print('<?xml version="1.0" ?>')60 -1 print('<playlist version="1" xmlns="http://xspf.org/ns/0/">')61 -1 print(' <trackList>')62 -1 for entry in iter_lines(args.src):63 -1 print(' <track>')64 -1 for k, v in sorted(entry.items()):65 -1 if k in KEYS:66 -1 print(' <{key}>{value}</{key}>'.format(67 -1 key=escape(KEYS[k]),68 -1 value=escape(unlist(v)),69 -1 ))70 -1 print(' </track>')71 -1 print(' </trackList>')72 -1 print('</playlist>')-1 57 args = parse_args() -1 58 -1 59 print('<?xml version="1.0" ?>') -1 60 print('<playlist version="1" xmlns="http://xspf.org/ns/0/">') -1 61 print(' <trackList>') -1 62 for entry in iter_lines(args.src): -1 63 print(' <track>') -1 64 for k, v in sorted(entry.items()): -1 65 if k in KEYS: -1 66 print(' <{key}>{value}</{key}>'.format( -1 67 key=escape(KEYS[k]), -1 68 value=escape(unlist(v)), -1 69 )) -1 70 print(' </track>') -1 71 print(' </trackList>') -1 72 print('</playlist>') 73 73 74 74 75 75 if __name__ == '__main__':76 -1 main()-1 76 main()
diff --git a/xspf2m3u.py b/xspf2m3u.py
@@ -9,15 +9,15 @@ from time import sleep 9 9 from xml.etree import ElementTree 10 10 11 11 try:12 -1 import youtube_dl-1 12 import youtube_dl 13 1314 -1 ydl = youtube_dl.YoutubeDL(params={15 -1 'noplaylist': True,16 -1 'quiet': True,17 -1 })18 -1 ydl_selector = ydl.build_format_selector('bestaudio/best')-1 14 ydl = youtube_dl.YoutubeDL(params={ -1 15 'noplaylist': True, -1 16 'quiet': True, -1 17 }) -1 18 ydl_selector = ydl.build_format_selector('bestaudio/best') 19 19 except ImportError:20 -1 youtube_dl = None-1 20 youtube_dl = None 21 21 22 22 NS = '{http://xspf.org/ns/0/}' 23 23 EXTS = ['mp3', 'ogg', 'opus', 'mp4', 'm4a', 'wav', 'flac', 'wma'] @@ -27,139 +27,141 @@ __version__ = '0.0.0' 27 27 28 28 29 29 class DownloadPool(set):30 -1 def log(self, s):31 -1 print(s, file=sys.stderr, end='\r')-1 30 def log(self, s): -1 31 print(s, file=sys.stderr, end='\r') 32 3233 -1 def download(self, url, path):34 -1 self.add(subprocess.Popen(['curl', url, '-s', '-o', path]))-1 33 def download(self, url, path): -1 34 self.add(subprocess.Popen(['curl', url, '-s', '-o', path])) 35 3536 -1 def poll(self):37 -1 for p in list(self):38 -1 if p.poll() is not None:39 -1 self.remove(p)40 -1 return len(self)-1 36 def poll(self): -1 37 for p in list(self): -1 38 if p.poll() is not None: -1 39 self.remove(p) -1 40 return len(self) 41 4142 -1 def __enter__(self):43 -1 return self-1 42 def __enter__(self): -1 43 return self 44 4445 -1 def __exit__(self, exc_type, exc_value, traceback):46 -1 if exc_type is None:47 -1 while self.poll():48 -1 self.log('{} downloads still active'.format(len(self)))49 -1 sleep(5)50 -1 else:51 -1 for p in self:52 -1 p.kill()-1 45 def __exit__(self, exc_type, exc_value, traceback): -1 46 if exc_type is None: -1 47 while self.poll(): -1 48 self.log('{} downloads still active'.format(len(self))) -1 49 sleep(5) -1 50 else: -1 51 for p in self: -1 52 p.kill() 53 53 54 54 55 55 def iter_files(folder):56 -1 for dirpath, dirnames, filenames in os.walk(folder):57 -1 for filename in filenames:58 -1 if filename.rsplit('.', 1)[-1] in EXTS:59 -1 yield dirpath, filename-1 56 for dirpath, dirnames, filenames in os.walk(folder): -1 57 for filename in filenames: -1 58 if filename.rsplit('.', 1)[-1] in EXTS: -1 59 yield dirpath, filename 60 60 61 61 62 62 def simplify(s):63 -1 s = s.lower()64 -1 s = s.encode('ascii', errors='replace').decode('ascii')65 -1 return CHARS.sub('', s)-1 63 s = s.lower() -1 64 s = s.encode('ascii', errors='replace').decode('ascii') -1 65 return CHARS.sub('', s) 66 66 67 67 68 68 def find_by_title(title, fields, files):69 -1 for dirpath, filename in files:70 -1 path = os.path.join(dirpath, filename)71 -1 if (72 -1 simplify(title) in simplify(filename)73 -1 and all(simplify(o) in simplify(path) for o in fields)74 -1 ):75 -1 return path-1 69 for dirpath, filename in files: -1 70 path = os.path.join(dirpath, filename) -1 71 if ( -1 72 simplify(title) in simplify(filename) -1 73 and all(simplify(o) in simplify(path) for o in fields) -1 74 ): -1 75 return path 76 76 77 77 78 78 def search_youtube(q):79 -1 # result expires in 6h80 -1 try:81 -1 info = ydl.extract_info('ytsearch:' + ' '.join(q), download=False)82 -1 except youtube_dl.utils.DownloadError:83 -1 return None, None84 -1 if not info['entries']:85 -1 return None, None86 -1 info = info['entries'][0]87 -1 try:88 -1 _format = next(ydl_selector({'formats': info['formats']}))89 -1 except StopIteration:90 -1 return None, None91 -1 filename = '{}-{}.{}'.format(92 -1 info['title'].replace('/', '_'),93 -1 info['id'],94 -1 _format['ext'],95 -1 )96 -1 return _format['url'], filename-1 79 # result expires in 6h -1 80 try: -1 81 info = ydl.extract_info('ytsearch:' + ' '.join(q), download=False) -1 82 except youtube_dl.utils.DownloadError: -1 83 return None, None -1 84 if not info['entries']: -1 85 return None, None -1 86 info = info['entries'][0] -1 87 try: -1 88 _format = next(ydl_selector({'formats': info['formats']})) -1 89 except StopIteration: -1 90 return None, None -1 91 filename = '{}-{}.{}'.format( -1 92 info['title'].replace('/', '_'), -1 93 info['id'], -1 94 _format['ext'], -1 95 ) -1 96 return _format['url'], filename 97 97 98 98 99 99 def iter_tracks(src):100 -1 root = ElementTree.parse(src).getroot()101 -1 for e in root.iter(NS + 'track'):102 -1 track = {}103 -1 for tag in ['location', 'title', 'creator', 'album', 'annotation']:104 -1 field = e.find(NS + tag)105 -1 if field is None:106 -1 track[tag] = None107 -1 else:108 -1 track[tag] = field.text109 -1 yield track-1 100 root = ElementTree.parse(src).getroot() -1 101 for e in root.iter(NS + 'track'): -1 102 track = {} -1 103 for tag in ['location', 'title', 'creator', 'album', 'annotation']: -1 104 field = e.find(NS + tag) -1 105 if field is None: -1 106 track[tag] = None -1 107 else: -1 108 track[tag] = field.text -1 109 yield track 110 110 111 111 112 112 def hard_link(location, outdir):113 -1 os.makedirs(outdir, exist_ok=True)114 -1 filename = os.path.basename(location)115 -1 path = os.path.join(outdir, filename)116 -1 os.link(location, path)117 -1 return path-1 113 os.makedirs(outdir, exist_ok=True) -1 114 filename = os.path.basename(location) -1 115 path = os.path.join(outdir, filename) -1 116 os.link(location, path) -1 117 return path 118 118 119 119 120 120 def parse_args():121 -1 parser = argparse.ArgumentParser()122 -1 parser.add_argument('src')123 -1 parser.add_argument('folder')124 -1 parser.add_argument('-Y', '--youtube', action='store_true')125 -1 parser.add_argument('-O', '--outdir')126 -1 return parser.parse_args()-1 121 parser = argparse.ArgumentParser() -1 122 parser.add_argument('src') -1 123 parser.add_argument('folder') -1 124 parser.add_argument('-Y', '--youtube', action='store_true') -1 125 parser.add_argument('-O', '--outdir') -1 126 return parser.parse_args() 127 127 128 128 129 129 def main():130 -1 args = parse_args()131 -1 files = list(iter_files(args.folder))132 -1133 -1 if args.outdir:134 -1 os.makedirs(args.outdir)135 -1136 -1 with DownloadPool() as pool:137 -1 for track in iter_tracks(args.src):138 -1 location = track['location']139 -1140 -1 if location is None:141 -1 context_key = ['creator', 'annotation']142 -1 context = [track[k] for k in context_key if track[k]]143 -1 location = find_by_title(track['title'], context, files)144 -1 if location and args.outdir:145 -1 location = hard_link(location, args.outdir)146 -1147 -1 if location is None and args.youtube and youtube_dl:148 -1 url, filename = search_youtube([q for q in track.values() if q])149 -1 if url is None:150 -1 location = None151 -1 elif args.outdir:152 -1 location = os.path.join(args.outdir, filename)153 -1 pool.download(url, location)154 -1 else:155 -1 location = url156 -1157 -1 if location is None:158 -1 s = ' - '.join('{}: {}'.format(k, v) for k, v in track.items())159 -1 print('# Warning: ' + s)160 -1 else:161 -1 print(location)-1 130 args = parse_args() -1 131 files = list(iter_files(args.folder)) -1 132 -1 133 if args.outdir: -1 134 os.makedirs(args.outdir) -1 135 -1 136 with DownloadPool() as pool: -1 137 for track in iter_tracks(args.src): -1 138 location = track['location'] -1 139 -1 140 if location is None: -1 141 context_key = ['creator', 'annotation'] -1 142 context = [track[k] for k in context_key if track[k]] -1 143 location = find_by_title(track['title'], context, files) -1 144 if location and args.outdir: -1 145 location = hard_link(location, args.outdir) -1 146 -1 147 if location is None and args.youtube and youtube_dl: -1 148 url, filename = search_youtube( -1 149 [q for q in track.values() if q] -1 150 ) -1 151 if url is None: -1 152 location = None -1 153 elif args.outdir: -1 154 location = os.path.join(args.outdir, filename) -1 155 pool.download(url, location) -1 156 else: -1 157 location = url -1 158 -1 159 if location is None: -1 160 s = ' - '.join('{}: {}'.format(k, v) for k, v in track.items()) -1 161 print('# Warning: ' + s) -1 162 else: -1 163 print(location) 162 164 163 165 164 166 if __name__ == '__main__':165 -1 main()-1 167 main()