mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-22 11:43:13 +00:00
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:
parent
b5110722fd
commit
9dbb2e5169
6 changed files with 128 additions and 13 deletions
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue