diff --git a/.gitignore b/.gitignore index 9c6e6a2..ac75c10 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,5 @@ tags # End of https://www.gitignore.io/api/vim,python,visualstudiocode + +test/test/* diff --git a/CommandExecutioner.py b/CommandExecutioner.py new file mode 100644 index 0000000..5b12e98 --- /dev/null +++ b/CommandExecutioner.py @@ -0,0 +1,211 @@ +from tinytag import TinyTag +import os +# from functools import reduce +import pprint +from binascii import b2a_hex + + +class CommandExecutioner: + def __init__(self, kwargs): + self.options = kwargs + + def work(self): + pass + + def scan(self): + """ + set self.files to be a list of files + element in list are in full path + """ + self.files = [] + + for root, dirs, files in os.walk(self.source): + print("[+] Scan {}".format(root)) + dirs[:] = [d for d in dirs if not d.startswith('.')] + for folder in dirs: + folder = os.path.join(root, folder) + for f in files: + self.files.append(os.path.join(root, f)) + + def _scan(self): + """ + deprecated + """ + # files to work on + # scan all files and store whole path on here + self.files = [] + + folder_queue = [self.source] + home_path_len = len(folder_queue[0]) + + while (len(folder_queue) > 0): + current_folder = folder_queue[0] + folder_queue = folder_queue[1:] + + if len(current_folder[home_path_len:]) == 0: + print("[+] Scan /") + else: + print("[+] Scan {}".format(current_folder[home_path_len:])) + + self.files += list( + filter( + lambda x: os.path.isfile(x), + os.listdir(current_folder)) + ) + for f in self.files: + f = current_folder + '/' + f + + folders = list( + filter( + lambda x: + os.path.isdir(x) and x[0] != '.', + os.listdir(current_folder))) + + for folder in folders: + folder_queue.append(current_folder + '/' + folder) + + def makedict(self): + """ + from list of files + to dictionary of file by attribute specified + """ + self.data = {} + for f in self.files: + try: + tag = TinyTag.get(f) + except LookupError: + # not a music file + continue + except BaseException: + print("Cannot get tag from file --> Skip\n\t{}".format(f)) + continue + tag = getattr(tag, self.attribute) + if tag in self.data: + self.data[tag].append(f) + else: + self.data[tag] = [] + self.data[tag].append(f) + + +class CommandSort(CommandExecutioner): + def __init__(self, kwargs): + super().__init__(kwargs) + self.source = kwargs['source'] + self.destination = kwargs['destination'] + self.attribute = kwargs['attribute'] + + def work(self): + self.scan() + self.makedict() + pprint.pprint(self.data) + self.sort() + + def sort(self): + # key is now the name of the folder + for folder_name, tracks in self.data.items(): + # print(folder_name) + # continue + + if folder_name is None: + # file attribute is None + folder = "Undefined" + elif folder_name == '': + # file attribute is empty string + folder = "Undefined" + else: + folder = folder_name + + # because folder is taken from file tags + # some tags could have '/' in it + # which is not acceptable as a file name in linux + # folder = folder.replace('/', '-') + + # print(self.destination) + folder = self.destination + '/' + folder + if not os.path.exists(folder): + os.makedirs(folder) + + print("========================================") + print("Album: {}".format(folder_name)) + print("Folder: {}".format(folder)) + # continue + for track in tracks: + new_file = folder + '/' + os.path.basename(track) + if track == new_file: + # after sort, stay the same + continue + if os.path.exists(new_file): + pass + else: + print("[+] Replace\n\t{}\n\t{}".format(track, new_file)) + # uncomment when you are ready + # os.rename(track, new_file) + pass + return + + +class CommandPlaylist(CommandExecutioner): + def __init__(self, kwargs): + super().__init__(kwargs) + self.source = kwargs['source'] + self.destination = kwargs['destination'] + self.attribute = kwargs['attribute'] + self.playlist = kwargs['playlist'] + + def work(self): + self.scan() + self.makedict() + print("Create playlist {}".format(self.playlist)) + + playlist = open(self.playlist, 'w') + playlist.write('#EXTM3U\n') + + for key, musics in self.data.items(): + for music in musics: + tag = TinyTag.get(music) + if tag.artist is None: + tag.artist = '' + if tag.title is None: + tag.title = '' + tag.duration = int(tag.duration) + + # write comment + playlist.write('#EXTINF:{},{} - {}\n' + .format( + tag.duration, + tag.artist, + tag.title)) + + # write file direction + music = encode_to_hex(music) + playlist.write('file://{}\n'.format(music)) + + print("{} created".format(self.playlist)) + + +def encode_to_hex(string): + """ + change special char to hex + """ + chars = list(string) + for i in range(len(string)): + hex_c = ord(chars[i]) + if hex_c >= ord('!') and hex_c <= ord('~'): + # skip ascii characters + # be aware of special ascii!!! + continue + elif hex_c == ord(' '): + chars[i] = '%20' + else: + # not ascii change to hex + u = b2a_hex(chars[i].encode('utf-8')).decode('utf-8') + u = list(u) + for j in range(len(u)): + u[j] = u[j].upper() + if j % 2 != 0: + continue + u[j] = '%' + u[j] + chars[i] = "".join(u) + + string = "".join(chars) + return string diff --git a/CommandHandler.py b/CommandHandler.py new file mode 100644 index 0000000..92db55f --- /dev/null +++ b/CommandHandler.py @@ -0,0 +1,90 @@ +import os +from CommandExecutioner import ( + CommandSort, + CommandPlaylist +) + + +argument_requirement = { + 'sort': + ['source', 'destination', 'attribute'], + 'playlist': + ['source', 'destination', 'attribute', 'playlist_name'], +} + +attributes = ('title', 'album', 'artist') + + +class CommandHandler: + """ + Pre validation on arugments + """ + + def __init__(self, kwargs): + # print(kwargs) + self.run(kwargs) + + def run(self, kwargs): + kwargs['source'] = os.path.abspath(kwargs['source']) + kwargs['destination'] = os.path.abspath(kwargs['destination']) + try: + self.validate(kwargs) + except Exception: + exit(-1) + + mode = kwargs['mode'] + worker = None + if mode == 'sort': + worker = CommandSort(kwargs) + elif mode == 'playlist': + worker = CommandPlaylist(kwargs) + else: + # not likely + return + + worker.work() + + def validate(self, kwargs): + requirements = argument_requirement[kwargs['mode']] + for requirement in requirements: + if requirement == 'source': + if not os.path.isdir(kwargs['source']): + print( + 'source:', + kwargs['source'], + 'is not a valid directory') + print('directory not exist or is not a directory') + raise Exception + if requirement == 'destination': + if not os.path.isdir(kwargs['destination']): + print( + 'destination:', + kwargs['destination'], + 'is not a valid directory') + print('directory not exist or is not a directory') + raise Exception + elif requirement == 'attribute': + if kwargs['attribute'] not in attributes: + print( + 'attribute:', + kwargs['attribute'], + 'is not a valid attribute') + print('valid attributes are:', attributes) + raise Exception + elif requirement == 'playlist_name': + if kwargs['playlist'] is None: + print("Playlist name must be given") + raise Exception + pl_file = kwargs['destination'] + \ + '/' + kwargs['playlist'] + '.m3u' + while True: + if not os.path.exists(pl_file): + break + overwrite = input( + 'File path {} is existed, overwrite?(y/n) ' + .format(pl_file)) + if overwrite in ('y', 'Y'): + break + newname = input("New file name: ") + pl_file = kwargs['destination'] + '/' + newname + '.m3u' + kwargs['playlist'] = pl_file diff --git a/run.py b/run.py new file mode 100644 index 0000000..19be55d --- /dev/null +++ b/run.py @@ -0,0 +1,46 @@ +# from .commandparser import commandparser +import click +import os +from CommandHandler import CommandHandler + + +@click.group() +# @click.option('--verbose', '-v', is_flag=True, default=False) +def main(): + pass + + +@main.command() +@click.option('--source', '-src', default=os.getcwd()) +@click.option('--destination', '-dst', default=os.getcwd() + '/dst/') +@click.option('--attribute', '-attr', default='album') +@click.option('--auto-overwrite', is_flag=True, default=False) +def sort(**kwargs): + commandparser('sort', **kwargs) + + +@main.command() +@click.option('--source', '-src', default=os.getcwd()) +@click.option('--destination', '-dst', default=os.getcwd() + '/dst/') +@click.option('--attribute', '-attr', default='album') +@click.option('--auto-overwrite', is_flag=True, default=False) +@click.option('--playlist', '-name') +def playlist(**kwargs): + commandparser('playlist', **kwargs) + + +@main.command() +@click.option('--source', '-src', default=os.getcwd()) +@click.option('--destination', '-dst', default=os.getcwd()) +@click.option('--format', '-fmt') +def format(**kwargs): + commandparser('format', **kwargs) + + +def commandparser(mode, **kwargs): + kwargs['mode'] = mode + CommandHandler(kwargs) + + +if __name__ == "__main__": + main()