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)
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
:class:`~plexapi.library.LibrarySection`.
"""
return self._server.createPlaylist(
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):
""" 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.base import Playable, PlexPartialObject
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.playqueue import PlayQueue
from plexapi.utils import deprecated
@ -375,15 +375,32 @@ class Playlist(
data = server.query(key, method=server._session.post)[0]
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
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.
Parameters:
server (:class:`~plexapi.server.PlexServer`): Server to create the playlist on.
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.
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.
@ -396,17 +413,23 @@ class Playlist(
See :func:`~plexapi.library.LibrarySection.search` for more info.
filters (dict): Smart playlists only, a dictionary of advanced filters.
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
search results. See :func:`~plexapi.library.LibrarySection.search` for more info.
Raises:
: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 attempting to import m3u file into non-music library.
:class:`plexapi.exceptions.BadRequest`: When failed to import m3u file.
Returns:
: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)
else:
return cls._create(server, title, items)

View file

@ -454,19 +454,42 @@ class PlexServer(PlexObject):
Returns:
: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(
self, title, section, items=items, smart=smart, limit=limit,
libtype=libtype, sort=sort, filters=filters, **kwargs)
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`.
Parameters:
title (str): Title of the playlist.
section (:class:`~plexapi.library.LibrarySection`, str): Smart playlists only,
library section to create the playlist in.
section (:class:`~plexapi.library.LibrarySection`, str): Smart playlists and m3u import only,
the library section to create the playlist in.
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.
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.
filters (dict): Smart playlists only, a dictionary of advanced filters.
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
search results. See :func:`~plexapi.library.LibrarySection.search` for more info.
Raises:
: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 attempting to import m3u file into non-music library.
:class:`plexapi.exceptions.BadRequest`: When failed to import m3u file.
Returns:
: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(
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):
""" Creates and returns a new :class:`~plexapi.playqueue.PlayQueue`.

View file

@ -306,6 +306,18 @@ def subtitle():
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()
def shared_username(account):
username = os.environ.get("SHARED_USERNAME", "PKKid")

View file

@ -259,6 +259,20 @@ def test_Playlist_exceptions(plex, movies, movie, artist):
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):
title = 'test_playlist_plexweburl'
episodes = show.episodes()

View file

@ -112,7 +112,7 @@ def clean_pms(server, path):
print("Deleted %s" % path)
def setup_music(music_path):
def setup_music(music_path, docker=False):
print("Setup files for the Music section..")
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 k, v in album.items():
artist_album = os.path.join(music_path, artist, k)
makedirs(artist_album, exist_ok=True)
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")))
@ -554,7 +565,7 @@ if __name__ == "__main__":
# Prepare Music section
if opts.with_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(
dict(