Create music playlist from m3u file (#1055)

* Create music playlists from m3u files

* Add test for playlist m3u import

* Add createCollection examples
This commit is contained in:
JonnyWong16 2022-12-21 11:32:43 -08:00 committed by GitHub
parent b5110722fd
commit 9dbb2e5169
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 128 additions and 13 deletions

View file

@ -1671,13 +1671,13 @@ class LibrarySection(PlexObject):
return self.search(libtype='collection', **kwargs) return self.search(libtype='collection', **kwargs)
def createPlaylist(self, title, items=None, smart=False, limit=None, def createPlaylist(self, title, items=None, smart=False, limit=None,
sort=None, filters=None, **kwargs): sort=None, filters=None, m3ufilepath=None, **kwargs):
""" Alias for :func:`~plexapi.server.PlexServer.createPlaylist` using this """ Alias for :func:`~plexapi.server.PlexServer.createPlaylist` using this
:class:`~plexapi.library.LibrarySection`. :class:`~plexapi.library.LibrarySection`.
""" """
return self._server.createPlaylist( return self._server.createPlaylist(
title, section=self, items=items, smart=smart, limit=limit, title, section=self, items=items, smart=smart, limit=limit,
sort=sort, filters=filters, **kwargs) sort=sort, filters=filters, m3ufilepath=m3ufilepath, **kwargs)
def playlist(self, title): def playlist(self, title):
""" Returns the playlist with the specified title. """ Returns the playlist with the specified title.

View file

@ -5,7 +5,7 @@ from urllib.parse import quote_plus, unquote
from plexapi import media, utils from plexapi import media, utils
from plexapi.base import Playable, PlexPartialObject from plexapi.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest, NotFound, Unsupported from plexapi.exceptions import BadRequest, NotFound, Unsupported
from plexapi.library import LibrarySection from plexapi.library import LibrarySection, MusicSection
from plexapi.mixins import SmartFilterMixin, ArtMixin, PosterMixin from plexapi.mixins import SmartFilterMixin, ArtMixin, PosterMixin
from plexapi.playqueue import PlayQueue from plexapi.playqueue import PlayQueue
from plexapi.utils import deprecated from plexapi.utils import deprecated
@ -375,15 +375,32 @@ class Playlist(
data = server.query(key, method=server._session.post)[0] data = server.query(key, method=server._session.post)[0]
return cls(server, data, initpath=key) return cls(server, data, initpath=key)
@classmethod
def _createFromM3U(cls, server, title, section, m3ufilepath):
""" Create a playlist from uploading an m3u file. """
if not isinstance(section, LibrarySection):
section = server.library.section(section)
if not isinstance(section, MusicSection):
raise BadRequest('Can only create playlists from m3u files in a music library.')
args = {'sectionID': section.key, 'path': m3ufilepath}
key = f"/playlists/upload{utils.joinArgs(args)}"
server.query(key, method=server._session.post)
try:
return server.playlists(sectionId=section.key, guid__endswith=m3ufilepath)[0].edit(title=title).reload()
except IndexError:
raise BadRequest('Failed to create playlist from m3u file.') from None
@classmethod @classmethod
def create(cls, server, title, section=None, items=None, smart=False, limit=None, def create(cls, server, title, section=None, items=None, smart=False, limit=None,
libtype=None, sort=None, filters=None, **kwargs): libtype=None, sort=None, filters=None, m3ufilepath=None, **kwargs):
""" Create a playlist. """ Create a playlist.
Parameters: Parameters:
server (:class:`~plexapi.server.PlexServer`): Server to create the playlist on. server (:class:`~plexapi.server.PlexServer`): Server to create the playlist on.
title (str): Title of the playlist. title (str): Title of the playlist.
section (:class:`~plexapi.library.LibrarySection`, str): Smart playlists only, section (:class:`~plexapi.library.LibrarySection`, str): Smart playlists and m3u import only,
the library section to create the playlist in. the library section to create the playlist in.
items (List): Regular playlists only, list of :class:`~plexapi.audio.Audio`, items (List): Regular playlists only, list of :class:`~plexapi.audio.Audio`,
:class:`~plexapi.video.Video`, or :class:`~plexapi.photo.Photo` objects to be added to the playlist. :class:`~plexapi.video.Video`, or :class:`~plexapi.photo.Photo` objects to be added to the playlist.
@ -396,17 +413,23 @@ class Playlist(
See :func:`~plexapi.library.LibrarySection.search` for more info. See :func:`~plexapi.library.LibrarySection.search` for more info.
filters (dict): Smart playlists only, a dictionary of advanced filters. filters (dict): Smart playlists only, a dictionary of advanced filters.
See :func:`~plexapi.library.LibrarySection.search` for more info. See :func:`~plexapi.library.LibrarySection.search` for more info.
m3ufilepath (str): Music playlists only, the full file path to an m3u file to import.
Note: This will overwrite any playlist previously created from the same m3u file.
**kwargs (dict): Smart playlists only, additional custom filters to apply to the **kwargs (dict): Smart playlists only, additional custom filters to apply to the
search results. See :func:`~plexapi.library.LibrarySection.search` for more info. search results. See :func:`~plexapi.library.LibrarySection.search` for more info.
Raises: Raises:
:class:`plexapi.exceptions.BadRequest`: When no items are included to create the playlist. :class:`plexapi.exceptions.BadRequest`: When no items are included to create the playlist.
:class:`plexapi.exceptions.BadRequest`: When mixing media types in the playlist. :class:`plexapi.exceptions.BadRequest`: When mixing media types in the playlist.
:class:`plexapi.exceptions.BadRequest`: When attempting to import m3u file into non-music library.
:class:`plexapi.exceptions.BadRequest`: When failed to import m3u file.
Returns: Returns:
:class:`~plexapi.playlist.Playlist`: A new instance of the created Playlist. :class:`~plexapi.playlist.Playlist`: A new instance of the created Playlist.
""" """
if smart: if m3ufilepath:
return cls._createFromM3U(server, title, section, m3ufilepath)
elif smart:
return cls._createSmart(server, title, section, limit, libtype, sort, filters, **kwargs) return cls._createSmart(server, title, section, limit, libtype, sort, filters, **kwargs)
else: else:
return cls._create(server, title, items) return cls._create(server, title, items)

View file

@ -454,19 +454,42 @@ class PlexServer(PlexObject):
Returns: Returns:
:class:`~plexapi.collection.Collection`: A new instance of the created Collection. :class:`~plexapi.collection.Collection`: A new instance of the created Collection.
Example:
.. code-block:: python
# Create a regular collection
movies = plex.library.section("Movies")
movie1 = movies.get("Big Buck Bunny")
movie2 = movies.get("Sita Sings the Blues")
collection = plex.createCollection(
title="Favorite Movies",
section=movies,
items=[movie1, movie2]
)
# Create a smart collection
collection = plex.createCollection(
title="Recently Aired Comedy TV Shows",
section="TV Shows",
smart=True,
sort="episode.originallyAvailableAt:desc",
filters={"episode.originallyAvailableAt>>": "4w", "genre": "comedy"}
)
""" """
return Collection.create( return Collection.create(
self, title, section, items=items, smart=smart, limit=limit, self, title, section, items=items, smart=smart, limit=limit,
libtype=libtype, sort=sort, filters=filters, **kwargs) libtype=libtype, sort=sort, filters=filters, **kwargs)
def createPlaylist(self, title, section=None, items=None, smart=False, limit=None, def createPlaylist(self, title, section=None, items=None, smart=False, limit=None,
libtype=None, sort=None, filters=None, **kwargs): libtype=None, sort=None, filters=None, m3ufilepath=None, **kwargs):
""" Creates and returns a new :class:`~plexapi.playlist.Playlist`. """ Creates and returns a new :class:`~plexapi.playlist.Playlist`.
Parameters: Parameters:
title (str): Title of the playlist. title (str): Title of the playlist.
section (:class:`~plexapi.library.LibrarySection`, str): Smart playlists only, section (:class:`~plexapi.library.LibrarySection`, str): Smart playlists and m3u import only,
library section to create the playlist in. the library section to create the playlist in.
items (List): Regular playlists only, list of :class:`~plexapi.audio.Audio`, items (List): Regular playlists only, list of :class:`~plexapi.audio.Audio`,
:class:`~plexapi.video.Video`, or :class:`~plexapi.photo.Photo` objects to be added to the playlist. :class:`~plexapi.video.Video`, or :class:`~plexapi.photo.Photo` objects to be added to the playlist.
smart (bool): True to create a smart playlist. Default False. smart (bool): True to create a smart playlist. Default False.
@ -478,19 +501,51 @@ class PlexServer(PlexObject):
See :func:`~plexapi.library.LibrarySection.search` for more info. See :func:`~plexapi.library.LibrarySection.search` for more info.
filters (dict): Smart playlists only, a dictionary of advanced filters. filters (dict): Smart playlists only, a dictionary of advanced filters.
See :func:`~plexapi.library.LibrarySection.search` for more info. See :func:`~plexapi.library.LibrarySection.search` for more info.
m3ufilepath (str): Music playlists only, the full file path to an m3u file to import.
Note: This will overwrite any playlist previously created from the same m3u file.
**kwargs (dict): Smart playlists only, additional custom filters to apply to the **kwargs (dict): Smart playlists only, additional custom filters to apply to the
search results. See :func:`~plexapi.library.LibrarySection.search` for more info. search results. See :func:`~plexapi.library.LibrarySection.search` for more info.
Raises: Raises:
:class:`plexapi.exceptions.BadRequest`: When no items are included to create the playlist. :class:`plexapi.exceptions.BadRequest`: When no items are included to create the playlist.
:class:`plexapi.exceptions.BadRequest`: When mixing media types in the playlist. :class:`plexapi.exceptions.BadRequest`: When mixing media types in the playlist.
:class:`plexapi.exceptions.BadRequest`: When attempting to import m3u file into non-music library.
:class:`plexapi.exceptions.BadRequest`: When failed to import m3u file.
Returns: Returns:
:class:`~plexapi.playlist.Playlist`: A new instance of the created Playlist. :class:`~plexapi.playlist.Playlist`: A new instance of the created Playlist.
Example:
.. code-block:: python
# Create a regular playlist
episodes = plex.library.section("TV Shows").get("Game of Thrones").episodes()
playlist = plex.createPlaylist(
title="GoT Episodes",
items=episodes
)
# Create a smart playlist
playlist = plex.createPlaylist(
title="Top 10 Unwatched Movies",
section="Movies",
smart=True,
limit=10,
sort="audienceRating:desc",
filters={"audienceRating>>": 8.0, "unwatched": True}
)
# Create a music playlist from an m3u file
playlist = plex.createPlaylist(
title="Favorite Tracks",
section="Music",
m3ufilepath="/path/to/playlist.m3u"
)
""" """
return Playlist.create( return Playlist.create(
self, title, section=section, items=items, smart=smart, limit=limit, self, title, section=section, items=items, smart=smart, limit=limit,
libtype=libtype, sort=sort, filters=filters, **kwargs) libtype=libtype, sort=sort, filters=filters, m3ufilepath=m3ufilepath, **kwargs)
def createPlayQueue(self, item, **kwargs): def createPlayQueue(self, item, **kwargs):
""" Creates and returns a new :class:`~plexapi.playqueue.PlayQueue`. """ Creates and returns a new :class:`~plexapi.playqueue.PlayQueue`.

View file

@ -306,6 +306,18 @@ def subtitle():
return handler return handler
@pytest.fixture()
def m3ufile(plex, music, track, tmp_path):
for path, paths, files in plex.walk(music.locations[0]):
for file in files:
if file.title == "playlist.m3u":
return file.path
m3u = tmp_path / "playlist.m3u"
with open(m3u, "w") as handler:
handler.write(track.media[0].parts[0].file)
return str(m3u)
@pytest.fixture() @pytest.fixture()
def shared_username(account): def shared_username(account):
username = os.environ.get("SHARED_USERNAME", "PKKid") username = os.environ.get("SHARED_USERNAME", "PKKid")

View file

@ -259,6 +259,20 @@ def test_Playlist_exceptions(plex, movies, movie, artist):
playlist.delete() playlist.delete()
def test_Playlist_m3ufile(plex, tvshows, music, m3ufile):
title = 'test_playlist_m3ufile'
try:
playlist = plex.createPlaylist(title, section=music.title, m3ufilepath=m3ufile)
assert playlist.title == title
finally:
playlist.delete()
with pytest.raises(BadRequest):
plex.createPlaylist(title, section=tvshows, m3ufilepath='does_not_exist.m3u')
with pytest.raises(BadRequest):
plex.createPlaylist(title, section=music, m3ufilepath='does_not_exist.m3u')
def test_Playlist_PlexWebURL(plex, show): def test_Playlist_PlexWebURL(plex, show):
title = 'test_playlist_plexweburl' title = 'test_playlist_plexweburl'
episodes = show.episodes() episodes = show.episodes()

View file

@ -112,7 +112,7 @@ def clean_pms(server, path):
print("Deleted %s" % path) print("Deleted %s" % path)
def setup_music(music_path): def setup_music(music_path, docker=False):
print("Setup files for the Music section..") print("Setup files for the Music section..")
makedirs(music_path, exist_ok=True) makedirs(music_path, exist_ok=True)
@ -135,12 +135,23 @@ def setup_music(music_path):
} }
m3u_file = open(os.path.join(music_path, "playlist.m3u"), "w")
for artist, album in all_music.items(): for artist, album in all_music.items():
for k, v in album.items(): for k, v in album.items():
artist_album = os.path.join(music_path, artist, k) artist_album = os.path.join(music_path, artist, k)
makedirs(artist_album, exist_ok=True) makedirs(artist_album, exist_ok=True)
for song in v: for song in v:
copyfile(STUB_MP3_PATH, os.path.join(artist_album, song)) trackpath = os.path.join(artist_album, song)
copyfile(STUB_MP3_PATH, trackpath)
if docker:
reltrackpath = os.path.relpath(trackpath, os.path.dirname(music_path))
m3u_file.write(os.path.join("/data", reltrackpath) + "\n")
else:
m3u_file.write(trackpath + "\n")
m3u_file.close()
return len(check_ext(music_path, (".mp3"))) return len(check_ext(music_path, (".mp3")))
@ -554,7 +565,7 @@ if __name__ == "__main__":
# Prepare Music section # Prepare Music section
if opts.with_music: if opts.with_music:
music_path = os.path.join(media_path, "Music") music_path = os.path.join(media_path, "Music")
song_c = setup_music(music_path) song_c = setup_music(music_path, docker=not opts.no_docker)
sections.append( sections.append(
dict( dict(