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)
|
||||
|
||||
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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue