diff --git a/vrobbler/apps/videos/management/commands/merge_duplicate_series.py b/vrobbler/apps/videos/management/commands/merge_duplicate_series.py new file mode 100644 index 0000000..351bc1d --- /dev/null +++ b/vrobbler/apps/videos/management/commands/merge_duplicate_series.py @@ -0,0 +1,84 @@ +from django.core.management.base import BaseCommand +from django.db import models + + +class Command(BaseCommand): + help = "Find TV Series with duplicate imdb_id values and merge videos to the newest series" + + def add_arguments(self, parser): + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be done without making changes", + ) + parser.add_argument( + "--auto", + action="store_true", + help="Automatically fix duplicates without prompting", + ) + + def handle(self, *args, **options): + from videos.models import Series + + dry_run = options["dry_run"] + auto = options["auto"] + + duplicate_imdb_ids = ( + Series.objects.filter(imdb_id__isnull=False) + .exclude(imdb_id="") + .values("imdb_id") + .annotate(count=models.Count("id")) + .filter(count__gt=1) + ) + + if not duplicate_imdb_ids.exists(): + self.stdout.write("No duplicate imdb_id values found in Series") + return + + self.stdout.write( + f"Found {duplicate_imdb_ids.count()} imdb_ids with duplicates in Series" + ) + + for dup in duplicate_imdb_ids: + imdb_id = dup["imdb_id"] + series_list = list( + Series.objects.filter(imdb_id=imdb_id).order_by("created") + ) + + self.stdout.write(f"\nimdb_id: {imdb_id}") + self.stdout.write(f" Found {len(series_list)} series:") + + for s in series_list: + video_count = s.video_set.count() + self.stdout.write( + f" id={s.id}, name='{s.name}', created={s.created}, videos={video_count}" + ) + + keeper = series_list[-1] + duplicates = series_list[:-1] + + self.stdout.write(f" Keeping: id={keeper.id} ('{keeper.name}')") + self.stdout.write(f" Merging from: {[d.id for d in duplicates]}") + + if dry_run: + self.stdout.write(self.style.WARNING(" [DRY RUN] Would move videos")) + continue + + if not auto: + confirm = input(" Move videos from duplicates to keeper? [y/N] ") + if confirm.lower() != "y": + self.stdout.write(" Skipped") + continue + + for dup_series in duplicates: + video_count = dup_series.video_set.count() + if video_count > 0: + dup_series.video_set.update(tv_series=keeper) + self.stdout.write( + f" Moved {video_count} videos from id={dup_series.id} to id={keeper.id}" + ) + + dup_series.delete() + self.stdout.write(f" Deleted series id={dup_series.id}") + + self.stdout.write(self.style.SUCCESS("\nDone!")) diff --git a/vrobbler/apps/videos/management/commands/merge_duplicate_series_by_name.py b/vrobbler/apps/videos/management/commands/merge_duplicate_series_by_name.py new file mode 100644 index 0000000..202d9bc --- /dev/null +++ b/vrobbler/apps/videos/management/commands/merge_duplicate_series_by_name.py @@ -0,0 +1,98 @@ +from django.core.management.base import BaseCommand +from django.db import models + + +class Command(BaseCommand): + help = "Find TV Series with duplicate names, merge Videos into the one with 'tt' IMDB id" + + def add_arguments(self, parser): + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be done without making changes", + ) + parser.add_argument( + "--auto", + action="store_true", + help="Automatically fix duplicates without prompting", + ) + + def handle(self, *args, **options): + from videos.models import Series + + dry_run = options["dry_run"] + auto = options["auto"] + + duplicate_names = ( + Series.objects.values("name") + .annotate(count=models.Count("id")) + .filter(count__gt=1) + ) + + if not duplicate_names.exists(): + self.stdout.write("No duplicate Series names found") + return + + self.stdout.write(f"Found {duplicate_names.count()} names with duplicates") + + for dup in duplicate_names: + name = dup["name"] + series_list = list(Series.objects.filter(name=name).order_by("created")) + + self.stdout.write(f"\nSeries name: {name}") + self.stdout.write(f" Found {len(series_list)} series:") + + for s in series_list: + video_count = s.video_set.count() + self.stdout.write( + f" id={s.id}, imdb_id='{s.imdb_id}', created={s.created}, videos={video_count}" + ) + + tt_series = None + for s in series_list: + if s.imdb_id and s.imdb_id.startswith("tt"): + tt_series = s + break + + if not tt_series: + self.stdout.write( + self.style.ERROR( + f" ERROR: No series with 'tt' IMDB id found, skipping" + ) + ) + continue + + duplicates = [s for s in series_list if s.id != tt_series.id] + self.stdout.write( + f" Keeper (tt): id={tt_series.id} ('{tt_series.imdb_id}')" + ) + self.stdout.write(f" Merging from: {[d.id for d in duplicates]}") + + if dry_run: + self.stdout.write(self.style.WARNING(" [DRY RUN] Would move videos")) + continue + + if not auto: + confirm = input(" Move videos from duplicates to keeper? [y/N] ") + if confirm.lower() != "y": + self.stdout.write(" Skipped") + continue + + total_moved = 0 + for dup_series in duplicates: + video_count = dup_series.video_set.count() + if video_count > 0: + dup_series.video_set.update(tv_series=tt_series) + self.stdout.write( + f" Moved {video_count} videos from id={dup_series.id} to id={tt_series.id}" + ) + total_moved += video_count + + dup_series.delete() + self.stdout.write(f" Deleted series id={dup_series.id}") + + self.stdout.write( + self.style.SUCCESS(f" Merged {total_moved} videos total") + ) + + self.stdout.write(self.style.SUCCESS("\nDone!"))