def run(self, args, config): mdb = self.get_metadata_db(args.tree) movies = sorted(mdb.itermovies(), key=lambda x: (x[1].get('title'), x[1].get('year'))) for movie_hash, movie in movies: printer.p(u'{hash} {movie}', hash=highlight_white(' ' + movie_hash + ' '), movie=unicode(movie))
def _import(self, mdb, mds, args, config, filename): printer.debug('Importing file {filename}', filename=filename) short_filename = os.path.basename(filename) title, ext = os.path.splitext(short_filename) year, title = clean_title(title) # Disable the year filter if auto mode is disabled: if not args.auto: year = None while True: # Disable the title input if auto mode is enabled: if not args.auto: title = printer.input(u'Title to search', default=title) datasource, movie = self._search(mds, title, short_filename, year, auto=args.auto) if datasource == 'manual': movie = self.profile.object_class() elif datasource == 'abort': printer.p('Aborted import of {filename}', filename=filename) return break if datasource is None: return return movie
def _import(self, mdb, mds, args, config, filename): printer.debug('Importing file {filename}', filename=filename) short_filename = os.path.basename(filename) title, ext = os.path.splitext(short_filename) title, season_num, episode_num = clean_title_series(title) while True: # Disable the title input if auto mode is enabled: if not args.auto: title = printer.input(u'Title to search', default=title) season_num = int(printer.input(u'Season number', default=season_num)) episode_num = int(printer.input(u'Episode number', default=episode_num)) datasource, episode = self._search(mds, title, short_filename, season_num, episode_num, auto=args.auto) if datasource == 'manual': episode = self.profile.object_class() elif datasource == 'abort': printer.p('Aborted import of {filename}', filename=filename) return break if datasource is None: return return episode # Refresh the full data for the choosen movie: episode = mds.refresh(episode)
def run(self, args, config): mdb = self.get_metadata_db(args.tree) for movie_input in args.input: movie_hash = get_hash(movie_input) try: movie = mdb.get(movie_hash) except KeyError: printer.p('Unknown movie hash.') return if args.unflag: for flag in self.unflag_unset_flags: try: del movie[flag] except KeyError: pass else: for flag in self.flags: movie[flag] = True for flag in self.unset_flags: try: del movie[flag] except KeyError: pass mdb.save(movie_hash, movie)
def _search(self, mdb, query, filename, season_num, episode_num, auto=False): """ Search the movie using all available datasources and let the user select a result. Return the choosen datasource and produced movie dict. If auto is enabled, directly returns the first movie found. """ choices = [] for datasource, movie in mdb.search(query, season=season_num, episode=episode_num): if auto: return datasource, movie fmt = u'<b>{title}</b> - <b>{ep}</b> S{season:02d}E{episode:02d} [{datasource}]' choices.append(option((datasource, movie), fmt, title=movie['title'], ep=movie['episode_title'], season=movie['season'], episode=movie['episode'], datasource=datasource.name)) if not choices: printer.p('No results to display for the file: {fn}', fn=filename) return None, None choices.append(option(('manual', None), 'Enter information manually')) choices.append(option(('abort', None), 'None of these')) printer.p('Please choose the relevant result for the file: {fn}', fn=filename, end='\n\n') return printer.choice(choices)
def run(self, args, config): mdb = self.get_metadata_db(args.tree) movie_hash = get_hash(args.input) try: movie = mdb.get(movie_hash) except KeyError: printer.p('Unknown movie hash.') return movie_json = json.dumps(movie, indent=4) while True: movie_json = printer.edit(movie_json) try: mdb.save(movie_hash, json.loads(movie_json)) except ValueError: if printer.ask('Bad json data, would you like to try again?', default=True): continue else: break else: printer.p('Saved.') break
def run(self, args, config): mdb = self.get_metadata_db(args.tree) mds = MovieDatasource(config.subsections('datasource'), args.tree, self.profile.object_class) if args.input is None: # Refresh all movies if printer.ask('Would you like to refresh all movies?', default=True): with printer.progress(mdb.count(), task=True) as update: for movie_hash, movie in list(mdb.itermovies()): movie = mds.refresh(movie) mdb.save(movie_hash, movie) printer.verbose('Saved {hash}', hash=movie_hash) update(1) else: movie_hash = get_hash(args.input) try: movie = mdb.get(movie_hash) except KeyError: printer.p('Unknown movie hash.') return else: movie = mds.refresh(movie) show(movie) if printer.ask('Would you like to save the movie?', default=True): mdb.save(movie_hash, movie) printer.p('Saved.')
def _search(self, mdb, query, filename, year=None, auto=False): """ Search the movie using all available datasources and let the user select a result. Return the choosen datasource and produced movie dict. If auto is enabled, directly returns the first movie found. """ choices = [] for datasource, movie in mdb.search(query, year=year): if auto: return datasource, movie if movie.get('directors'): directors = ' by ' if len(movie['directors']) > 1: directors += '%s and %s' % (', '.join(movie['directors'][0:-1]), movie['directors'][-1]) else: directors += movie['directors'][0] else: directors = '' fmt = u'<b>{title}</b> ({year}){directors} [{datasource}]' choices.append(option((datasource, movie), fmt, title=movie['title'], year=movie.get('year', 'Unknown'), directors=directors, datasource=datasource.name)) if not choices: printer.p('No results to display for the file: {fn}', fn=filename) return None, None choices.append(option(('manual', None), 'Enter information manually')) choices.append(option(('abort', None), 'None of these')) printer.p('Please choose the relevant movie for the file: {fn}', fn=filename, end='\n\n') return printer.choice(choices)
def run(self, args, config): config_fullname = os.path.join(args.tree, '.kolekto', 'config') with open(config_fullname, 'r+') as fconfig: config = printer.edit(fconfig.read()) fconfig.seek(0) fconfig.truncate() fconfig.write(config) printer.p('Saved.')
def run(self, args, config): mdb = self.get_metadata_db(args.tree) with open(args.file) as fdump: dump = json.load(fdump) for movie in dump: mdb.save(movie['hash'], movie['movie']) printer.verbose('Loaded {hash}', hash=movie['hash']) printer.p('Loaded {nb} movies.', nb=len(dump))
def run(self, args, config): mdb = self.get_metadata_db(args.tree) hash_by_title = defaultdict(lambda: []) for movie_hash, movie in mdb.itermovies(): hash_by_title[movie.get('title', None), movie.get('year', None)].append(movie_hash) for (title, year), hashs in hash_by_title.iteritems(): if len(hashs) > 1: printer.p('{title} ({year}): {hashs}', title=bold(title), year=year, hashs=' '.join(hashs))
def run(self, args, config): mdb = self.get_metadata_db(args.tree) mds = MovieDatasource(config.subsections('datasource'), args.tree, self.profile.object_class) listing = self._config(args, config) def _sorter((movie_hash, movie)): return tuple(movie.get(x) for x in listing['order']) movies = sorted(mdb.itermovies(), key=_sorter) # Get the current used listing: for movie_hash, movie in movies: movie = mds.attach(movie_hash, movie) prepared_env = parse_pattern(listing['pattern'], movie, ListingFormatWrapper) printer.p(u'<inv><b> {hash} </b></inv> ' + listing['pattern'], hash=movie_hash, **prepared_env)
def _import(self, mdb, mds, args, config, filename): printer.debug('Importing file {filename}', filename=filename) short_filename = os.path.basename(filename) title, ext = os.path.splitext(short_filename) year, title = clean_title(title) # Disable the year filter if auto mode is disabled: if not args.auto: year = None while True: # Disable the title input if auto mode is enabled: if not args.auto: title = printer.input(u'Title to search', default=title) datasource, movie = self._search(mds, title, short_filename, year, auto=args.auto) if datasource == 'manual': movie = self.profile.object_class elif datasource == 'abort': printer.p('Aborted import of {filename}', filename=filename) return break if datasource is None: return # Refresh the full data for the choosen movie: movie = mds.refresh(movie) if args.show: show(movie) printer.p('') # Edit available data: if not args.auto and printer.ask('Do you want to edit the movie metadata', default=False): movie = self.profile.object_class(json.loads(printer.edit(json.dumps(movie, indent=True)))) # Hardlink or copy the movie in the tree if args.hardlink or args.symlink: printer.p('\nComputing movie sha1sum...') movie_hash = link(args.tree, filename, args.symlink) else: printer.p('\nCopying movie in kolekto tree...') movie_hash = copy(args.tree, filename) printer.p('') mdb.save(movie_hash, movie) printer.debug('Movie {hash} saved to the database', hash=movie_hash) if args.delete: os.unlink(filename) printer.debug('Deleted original file {filename}', filename=filename)
def run(self, args, config): mds = MovieDatasource(config.subsections('datasource'), args.tree, self.profile.object_class) mdb = self.get_metadata_db(args.tree) hash_by_title = defaultdict(lambda: []) for movie_hash, movie in mdb.itermovies(): movie = mds.attach(movie_hash, movie) hash_info = '<inv> %s </inv> (%s/%s)' % (movie_hash, movie.get('quality'), movie.get('ext')) hash_by_title[movie.get('title', None), movie.get('year', None)].append(hash_info) for (title, year), hashs in hash_by_title.iteritems(): if len(hashs) > 1: printer.p('<b>{title}</b> ({year}): {hashs}', title=title, year=year, hashs=' '.join(hashs))
def run(self, args, config): mdb = self.get_metadata_db(args.tree) mds = MovieDatasource(config.subsections('datasource'), args.tree, self.profile.object_class) movie_hash = get_hash(args.input) try: movie = mdb.get(movie_hash) except KeyError: printer.p('Unknown movie hash.') return movie = mds.attach(movie_hash, movie) show(movie)
def show(movie): """ Show the movie metadata. """ for key, value in sorted(movie.iteritems(), cmp=metadata_sorter, key=lambda x: x[0]): if isinstance(value, list): if not value: continue other = value[1:] value = value[0] else: other = [] printer.p('{key}: {value}', key=bold(key), value=value) for value in other: printer.p('{pad}{value}', value=value, pad=' ' * (len(key) + 2))
def run(self, args, config): mdb = self.get_metadata_db(args.tree) movie_hash = get_hash(args.input) try: mdb.get(movie_hash) except KeyError: printer.p('Unknown movie hash.') return if printer.ask('Are you sure?', default=False): mdb.remove(movie_hash) printer.p('Removed. You need to launch gc to free space.')
def run(self, args, config): if config is not None: raise KolektoRuntimeError('Already a Kolekto tree') movies_directory = os.path.join(args.tree, '.kolekto', 'movies') # Ensure that the .kolekto/movies directory exists: if not os.path.isdir(movies_directory): os.makedirs(movies_directory) # Write the default config: with open(os.path.join(args.tree, '.kolekto', 'config'), 'w') as fconfig: fconfig.write(DEFAULT_CONFIG.format(profile=args.profile)) printer.p('Initialized empty Kolekto tree in {where}.', where=os.path.abspath(args.tree)) # Open the metadata db to create it automatically: self.get_metadata_db(args.tree)
def run(self, args, config): mdb = self.get_metadata_db(args.tree) db_files = set() for movie_hash, movie in mdb.itermovies(): db_files.add(movie_hash) db_files.update(movie.get("_externals", [])) printer.verbose("Found {nb} files in database", nb=len(db_files)) fs_files = set(os.listdir(os.path.join(args.tree, ".kolekto", "movies"))) printer.verbose("Found {nb} files in filesystem", nb=len(fs_files)) orphan_files = fs_files - db_files printer.p("Found {nb} orphan files to delete", nb=len(orphan_files)) if orphan_files: printer.verbose("Files to delete: {files}", files=", ".join(orphan_files)) if printer.ask("Would you like to delete orphans?"): for orphan_file in orphan_files: try: os.remove(os.path.join(args.tree, ".kolekto", "movies", orphan_file)) except OSError as err: printer.p("Unable to delete {file}: {err}", file=orphan_file, err=err) else: printer.verbose("Deleted {file}", file=orphan_file)
def run(self, args, config): # Check the args: if args.symlink and args.delete: raise KolektoRuntimeError('--delete can\'t be used with --symlink') elif args.symlink and args.hardlink: raise KolektoRuntimeError('--symlink and --hardlink are mutually exclusive') # Load the metadata database: mdb = self.get_metadata_db(args.tree) # Load informations from db: mds = MovieDatasource(config.subsections('datasource'), args.tree, self.profile.object_class) attachment_store = AttachmentStore(os.path.join(args.tree, '.kolekto', 'attachments')) for filename in args.file: filename = filename.decode('utf8') movie = self._import(mdb, mds, args, config, filename) # Refresh the full data for the choosen movie: movie = mds.refresh(movie) # Append the import date movie['import_date'] = datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S') if args.show: show(movie) printer.p('') # Edit available data: if not args.auto and printer.ask('Do you want to edit the movie metadata', default=False): movie = self.profile.object_class(json.loads(printer.edit(json.dumps(movie, indent=True)))) # Hardlink or copy the movie in the tree if args.hardlink or args.symlink: printer.p('\nComputing movie sha1sum...') movie_hash = link(args.tree, filename, args.symlink) else: printer.p('\nCopying movie in kolekto tree...') movie_hash = copy(args.tree, filename) printer.p('') mdb.save(movie_hash, movie) printer.debug('Movie {hash} saved to the database', hash=movie_hash) if args.delete: os.unlink(filename) printer.debug('Deleted original file {filename}', filename=filename) # Import the attachments if movie_hash is not None and args.import_attachments: attachments = list_attachments(filename) if attachments: printer.p('Found {nb} attachment(s) for this movie:', nb=len(attachments)) for attach in attachments: printer.p(' - {filename}', filename=attach) if not args.auto and printer.ask('Import them?', default=True): for attach in attachments: _, ext = os.path.splitext(attach) attachment_store.store(movie_hash, ext.lstrip('.'), open(attach))
def run(self, args, config): mdb = self.get_metadata_db(args.tree) mds = MovieDatasource(config.subsections('datasource'), args.tree, self.profile.object_class) total_runtime = 0 total_size = 0 count_by_genre = defaultdict(lambda: 0) count_by_director = defaultdict(lambda: 0) count_by_quality = defaultdict(lambda: 0) count_by_container = defaultdict(lambda: 0) for movie_hash, movie in mdb.itermovies(): movie_fullpath = os.path.join(args.tree, '.kolekto', 'movies', movie_hash) movie = mds.attach(movie_hash, movie) total_runtime += movie.get('runtime', 0) total_size += os.path.getsize(movie_fullpath) for genre in movie.get('genres', []): count_by_genre[genre] += 1 for director in movie.get('directors', []): count_by_director[director] += 1 count_by_quality[movie.get('quality', 'n/a')] += 1 count_by_container[movie.get('container', 'n/a')] += 1 printer.p(bold('Number of movies:'), mdb.count()) printer.p(bold('Total runtime:'), timedelta(seconds=total_runtime * 60)) printer.p(bold('Total size:'), humanize_filesize(total_size)) printer.p(bold('Genres top3:'), format_top(count_by_genre)) printer.p(bold('Director top3:'), format_top(count_by_director)) printer.p(bold('Quality:'), format_top(count_by_quality, None)) printer.p(bold('Container:'), format_top(count_by_container, None))
def run(self, args, config): mdb = self.get_metadata_db(args.tree) mds = MovieDatasource(config.subsections('datasource'), args.tree, self.profile.object_class) if args.dry_run: printer.p('Dry run: I will not create or delete any link') # Create the list of links that must exists on the fs: db_links = {} with printer.progress(mdb.count(), task=True) as update: for movie_hash, movie in mdb.itermovies(): movie = mds.attach(movie_hash, movie) for view in config.subsections('view'): for pattern in view.get('pattern'): for result in format_all(pattern, movie): filename = os.path.join(view.args, result) if filename in db_links: printer.p('Warning: duplicate link {link}', link=filename) else: db_links[filename] = movie_hash update(1) # Create the list of links already existing on the fs: fs_links = {} for view in config.subsections('view'): view_links = walk_links(os.path.join(args.tree, view.args), prefix=view.args, linkbase=os.path.join(args.tree, '.kolekto', 'movies')) fs_links.update(view_links) db_links = set(db_links.iteritems()) fs_links = set(fs_links.iteritems()) links_to_delete = fs_links - db_links links_to_create = db_links - fs_links printer.p('Found {rem} links to delete, {add} links to create', rem=len(links_to_delete), add=len(links_to_create)) dirs_to_cleanup = set() # Delete the old links: for filename, link in links_to_delete: printer.verbose('Deleting {file}', file=filename) if not args.dry_run: os.remove(os.path.join(args.tree, filename)) while filename: filename = os.path.split(filename)[0] dirs_to_cleanup.add(filename) dirs_to_cleanup.discard('') # Avoid to delete view roots # Delete empty directories: for directory in dirs_to_cleanup: if not args.dry_run: try: os.rmdir(os.path.join(args.tree, directory)) except OSError, err: if err.errno != 39: # Ignore "Directory not empty" error raise else: printer.verbose('Deleted directory {dir}', dir=directory)