add a command to create playlist from folder structure
This commit is contained in:
parent
c24b6ee183
commit
cf32e16547
|
@ -0,0 +1,150 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
import mutagen
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from funkwhale_api.music import models, utils
|
||||||
|
from funkwhale_api.playlists import models as playlist_models
|
||||||
|
from funkwhale_api.users import models as user_models
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_playlist(self, playlist_name, user, **options):
|
||||||
|
playlist = playlist_models.Playlist.objects.filter(
|
||||||
|
Q(user=user) & Q(name=playlist_name)
|
||||||
|
).first()
|
||||||
|
if not playlist:
|
||||||
|
if options["no_dry_run"]:
|
||||||
|
playlist = playlist_models.Playlist.objects.create(
|
||||||
|
name=playlist_name, user=user, privacy_level=options["privacy_level"]
|
||||||
|
)
|
||||||
|
return playlist
|
||||||
|
|
||||||
|
response = input(
|
||||||
|
f"This playlist {playlist_name} will be created. Proceed? (y/n): "
|
||||||
|
)
|
||||||
|
if response.lower() in "yes":
|
||||||
|
playlist = playlist_models.Playlist.objects.create(
|
||||||
|
name=playlist_name, user=user, privacy_level=options["privacy_level"]
|
||||||
|
)
|
||||||
|
return playlist
|
||||||
|
else:
|
||||||
|
return playlist
|
||||||
|
|
||||||
|
|
||||||
|
def get_fw_track_list(self, directory, playlist, **options):
|
||||||
|
fw_tracks = []
|
||||||
|
audio_extensions = utils.SUPPORTED_EXTENSIONS
|
||||||
|
existing_tracks = playlist.playlist_tracks.select_for_update()
|
||||||
|
for file in next(os.walk(directory))[2]:
|
||||||
|
if file.endswith(tuple(audio_extensions)):
|
||||||
|
track_path = os.path.join(directory, file)
|
||||||
|
try:
|
||||||
|
audio = mutagen.File(track_path)
|
||||||
|
except mutagen.MutagenError as e:
|
||||||
|
self.stdout.write(
|
||||||
|
f"Could not load {track_path} because of a mutagen exception : {e}"
|
||||||
|
)
|
||||||
|
if options["only_mbid"]:
|
||||||
|
mbid = (
|
||||||
|
audio.get("UFID:http://musicbrainz.org", None).data.decode()
|
||||||
|
if audio.get("UFID:http://musicbrainz.org", None)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if not mbid:
|
||||||
|
self.stdout.write(
|
||||||
|
f"Did not find mbid, skipping track {track_path}..."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
track_fw = models.Track.objects.get(mbid=mbid)
|
||||||
|
except models.Track.DoesNotExist:
|
||||||
|
self.stdout.write(f"No track found for {track_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.stdout.write(f"rack_path {str(track_path)}...")
|
||||||
|
|
||||||
|
track_fw = models.Upload.objects.get(source=track_path)
|
||||||
|
except models.Upload.DoesNotExist:
|
||||||
|
self.stdout.write(f"No track found for {track_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if existing_tracks.filter(track__id=track_fw.id).exists():
|
||||||
|
self.stdout.write(
|
||||||
|
f"Track already in playlist. Skipping {track_path}..."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
fw_tracks.append(track_fw)
|
||||||
|
|
||||||
|
return fw_tracks
|
||||||
|
|
||||||
|
|
||||||
|
def add_tracks_to_playlist(self, directory, user, **options):
|
||||||
|
playlist_name = os.path.basename(directory)
|
||||||
|
playlist = get_or_create_playlist(self, playlist_name, user, **options)
|
||||||
|
|
||||||
|
fw_track_list = get_fw_track_list(self, directory, playlist, **options)
|
||||||
|
if options["no_dry_run"] is True:
|
||||||
|
return playlist.insert_many(fw_track_list, allow_duplicates=False)
|
||||||
|
|
||||||
|
response = input(
|
||||||
|
f"These tracks {fw_track_list} will be added to playlist {playlist_name}. Proceed? (y/n): "
|
||||||
|
)
|
||||||
|
if response.lower() in "yes":
|
||||||
|
return playlist.insert_many(fw_track_list, allow_duplicates=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = """
|
||||||
|
This command creates playlists based on a folder structure. It uses the base folder
|
||||||
|
of each track as the playlist name. Subdirectories are taken into account but generate independent
|
||||||
|
playlists. Tracks contained in subdirectories don't appear in the parent directory playlist.
|
||||||
|
You will be asked to confirm the action before the playlist is created. Duplicate content in the
|
||||||
|
playlist isn't supported.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--user_name",
|
||||||
|
help="User name that will own the playlists",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dir_name",
|
||||||
|
help="Which directory to start from.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--privacy_level",
|
||||||
|
default="me",
|
||||||
|
choices=["me", "instance", "everyone"],
|
||||||
|
help="Which privacy_level for the playlists.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no_dry_run",
|
||||||
|
default=False,
|
||||||
|
help="Will actually write data into the database",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--only_mbid",
|
||||||
|
default=False,
|
||||||
|
help='Only files tagged with mbid will be used. Can be useful to create playlist from folders \
|
||||||
|
that are not "in-place" imported into funkwhale',
|
||||||
|
)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
all_subdirectories = []
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(options["dir_name"]):
|
||||||
|
for dir_name in dirs:
|
||||||
|
full_dir_path = os.path.join(root, dir_name)
|
||||||
|
all_subdirectories.append(full_dir_path)
|
||||||
|
user = user_models.User.objects.get(username=options["user_name"])
|
||||||
|
|
||||||
|
for directory in all_subdirectories:
|
||||||
|
add_tracks_to_playlist(self, directory, user, **options)
|
|
@ -5,10 +5,12 @@ import pytest
|
||||||
from funkwhale_api.common import utils as common_utils
|
from funkwhale_api.common import utils as common_utils
|
||||||
from funkwhale_api.music.management.commands import (
|
from funkwhale_api.music.management.commands import (
|
||||||
check_inplace_files,
|
check_inplace_files,
|
||||||
|
create_playlist_from_folder_structure,
|
||||||
fix_uploads,
|
fix_uploads,
|
||||||
prune_library,
|
prune_library,
|
||||||
prune_non_mbid_content,
|
prune_non_mbid_content,
|
||||||
)
|
)
|
||||||
|
from funkwhale_api.playlists import models as playlist_models
|
||||||
|
|
||||||
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
@ -247,3 +249,23 @@ def test_prune_non_mbid_content(factories):
|
||||||
|
|
||||||
for t in not_prunable:
|
for t in not_prunable:
|
||||||
t.refresh_from_db()
|
t.refresh_from_db()
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_playlist_from_folder_structure(factories, tmp_path):
|
||||||
|
user = factories["users.User"]()
|
||||||
|
c = create_playlist_from_folder_structure.Command()
|
||||||
|
options = {
|
||||||
|
"dir_name": DATA_DIR,
|
||||||
|
"user_name": user.username,
|
||||||
|
"privacy_level": "me",
|
||||||
|
"yes": True,
|
||||||
|
"no_dry_run": True,
|
||||||
|
"only_mbid": False,
|
||||||
|
}
|
||||||
|
c.handle(**options)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
playlist_models.Playlist.objects.all()
|
||||||
|
.filter(name="test_directory_playlist")
|
||||||
|
.exists()
|
||||||
|
)
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
||||||
|
Add a command to create playlists from folder structure (#2223)
|
Loading…
Reference in New Issue