python-plexapi/tests/test_library.py
blacktwin 5494c1e90d Change test to use Other Videos library type to prevent scanning
Tests:
 Create library with multiple paths with valid paths
 Create library with multiple paths with an invalid path
 Create library with no path
 Add path(s) to test library with an invalid path
 Remove path(s) to test library with an invalid path
 Remove path(s) to test library with valid paths
 Add path(s) to test library with valid paths
2021-12-20 00:12:51 -05:00

780 lines
26 KiB
Python

# -*- coding: utf-8 -*-
from collections import namedtuple
from datetime import datetime, timedelta
from urllib.parse import quote_plus
import pytest
import plexapi.base
from plexapi.exceptions import BadRequest, NotFound
from . import conftest as utils
def test_library_Library_section(plex):
sections = plex.library.sections()
assert len(sections) >= 3
section_name = plex.library.section("TV Shows")
assert section_name.title == "TV Shows"
with pytest.raises(NotFound):
assert plex.library.section("cant-find-me")
with pytest.raises(NotFound):
assert plex.library.sectionByID(-1)
def test_library_Library_sectionByID_is_equal_section(plex, movies):
# test that sctionmyID refreshes the section if the key is missing
# this is needed if there isnt any cached sections
assert plex.library.sectionByID(movies.key).uuid == movies.uuid
def test_library_sectionByID_with_attrs(plex, movies):
assert movies.agent == "tv.plex.agents.movie"
# This seems to fail for some reason.
# my account alloew of sync, didnt find any about settings about the library.
# assert movies.allowSync is ("sync" in plex.ownerFeatures)
assert movies.art == "/:/resources/movie-fanart.jpg"
assert utils.is_metadata(
movies.composite, prefix="/library/sections/", contains="/composite/"
)
assert utils.is_datetime(movies.createdAt)
assert movies.filters is True
assert movies._initpath == "/library/sections"
assert utils.is_int(movies.key)
assert movies.language == "en-US"
assert len(movies.locations) == 1
assert len(movies.locations[0]) >= 10
assert movies.refreshing is False
assert movies.scanner == "Plex Movie"
assert movies._server._baseurl == utils.SERVER_BASEURL
assert movies.thumb == "/:/resources/movie.png"
assert movies.title == "Movies"
assert movies.type == "movie"
assert utils.is_datetime(movies.updatedAt)
assert len(movies.uuid) == 36
def test_library_section_get_movie(movies):
assert movies.get("Sita Sings the Blues")
def test_library_MovieSection_getGuid(movies, movie):
result = movies.getGuid(guid=movie.guids[0].id)
assert result == movie
def test_library_section_movies_all(movies):
assert movies.totalSize == 4
assert len(movies.all(container_start=0, container_size=1, maxresults=1)) == 1
def test_library_section_movies_all_guids(movies):
plexapi.base.USER_DONT_RELOAD_FOR_KEYS.add('guids')
try:
results = movies.all(includeGuids=False)
assert results[0].guids == []
results = movies.all()
assert results[0].guids
movie = movies.get("Sita Sings the Blues")
assert movie.guids
finally:
plexapi.base.USER_DONT_RELOAD_FOR_KEYS.remove('guids')
def test_library_section_totalDuration(tvshows):
assert utils.is_int(tvshows.totalDuration)
def test_library_section_totalStorage(tvshows):
assert utils.is_int(tvshows.totalStorage)
def test_library_section_totalViewSize(tvshows):
assert tvshows.totalViewSize() == 2
assert tvshows.totalViewSize(libtype="show") == 2
assert tvshows.totalViewSize(libtype="season") == 4
assert tvshows.totalViewSize(libtype="episode") == 49
show = tvshows.get("The 100")
show.addCollection("test_view_size")
assert tvshows.totalViewSize() == 3
assert tvshows.totalViewSize(includeCollections=False) == 2
show.removeCollection("test_view_size", locked=False)
def test_library_section_delete(movies, patched_http_call):
movies.delete()
def test_library_fetchItem(plex, movie):
item1 = plex.library.fetchItem("/library/metadata/%s" % movie.ratingKey)
item2 = plex.library.fetchItem(movie.ratingKey)
assert item1.title == "Elephants Dream"
assert item1 == item2 == movie
def test_library_onDeck(plex, movie):
movie.updateProgress(movie.duration // 4) # set progress to 25%
assert movie in plex.library.onDeck()
movie.markUnwatched()
def test_library_recentlyAdded(plex):
assert len(list(plex.library.recentlyAdded()))
def test_library_add_edit_delete(plex, movies, photos):
# Create Other Videos library = No external metadata scanning
section_name = "plexapi_test_section"
movie_location = movies.locations[0]
photo_location = photos.locations[0]
plex.library.add(
name=section_name,
type="movie",
agent="com.plexapp.agents.none",
scanner="Plex Video Files Scanner",
language="en",
location=[movie_location, photo_location]
)
section = plex.library.section(section_name)
assert section.title == section_name
# Create library with an invalid path
error_section_name = "plexapi_error_section"
with pytest.raises(BadRequest):
plex.library.add(
name=error_section_name,
type="movie",
agent="com.plexapp.agents.none",
scanner="Plex Video Files Scanner",
language="en",
location=[movie_location, photo_location[:-1]]
)
# Create library with no path
with pytest.raises(BadRequest):
plex.library.add(
name=error_section_name,
type="movie",
agent="com.plexapp.agents.none",
scanner="Plex Video Files Scanner",
language="en",
)
with pytest.raises(BadRequest):
plex.library.section(error_section_name)
new_title = "a renamed lib"
section.edit(name=new_title)
section.reload()
assert section.title == new_title
with pytest.raises(BadRequest):
section.addLocations(movie_location[:-1])
with pytest.raises(BadRequest):
section.removeLocations(movie_location[:-1])
section.removeLocations(photo_location)
section.reload()
assert len(section.locations) == 1
section.addLocations(photo_location)
section.reload()
assert len(section.locations) == 2
section.delete()
assert section not in plex.library.sections()
def test_library_Library_cleanBundle(plex):
plex.library.cleanBundles()
def test_library_Library_optimize(plex):
plex.library.optimize()
def test_library_Library_emptyTrash(plex):
plex.library.emptyTrash()
def _test_library_Library_refresh(plex):
# TODO: fix mangle and proof the sections attrs
plex.library.refresh()
def test_library_Library_update(plex):
plex.library.update()
def test_library_Library_cancelUpdate(plex):
plex.library.cancelUpdate()
def test_library_Library_deleteMediaPreviews(plex):
plex.library.deleteMediaPreviews()
def test_library_Library_all(plex):
assert len(plex.library.all(title__iexact="The 100"))
def test_library_Library_search(plex):
item = plex.library.search("Elephants Dream")[0]
assert item.title == "Elephants Dream"
assert len(plex.library.search(libtype="episode"))
def test_library_MovieSection_update(movies):
movies.update()
def test_library_MovieSection_update_path(movies):
movies.update(path=movies.locations[0])
def test_library_MovieSection_refresh(movies, patched_http_call):
movies.refresh()
def test_library_MovieSection_search_genre(movie, movies):
genre = movie.genres[0]
assert len(movies.search(genre=genre)) >= 1
def test_library_MovieSection_cancelUpdate(movies):
movies.cancelUpdate()
def test_librarty_deleteMediaPreviews(movies):
movies.deleteMediaPreviews()
def test_library_MovieSection_onDeck(movie, movies, tvshows, episode):
movie.updateProgress(movie.duration // 4) # set progress to 25%
assert movie in movies.onDeck()
movie.markUnwatched()
episode.updateProgress(episode.duration // 4)
assert episode in tvshows.onDeck()
episode.markUnwatched()
def test_library_MovieSection_searchMovies(movies):
assert movies.searchMovies(title="Elephants Dream")
def test_library_MovieSection_recentlyAdded(movies, movie):
assert movie in movies.recentlyAdded()
assert movie in movies.recentlyAddedMovies()
def test_library_MovieSection_analyze(movies):
movies.analyze()
def test_library_MovieSection_collections(movies, movie):
try:
collection = movies.createCollection("test_library_MovieSection_collections", movie)
collections = movies.collections()
assert len(collections)
assert collection in collections
c = movies.collection(collection.title)
assert collection == c
finally:
collection.delete()
def test_library_MovieSection_collection_exception(movies):
with pytest.raises(NotFound):
movies.collection("Does Not Exists")
def test_library_MovieSection_PlexWebURL(plex, movies):
tab = 'library'
url = movies.getWebURL(tab=tab)
assert url.startswith('https://app.plex.tv/desktop')
assert plex.machineIdentifier in url
assert 'source=%s' % movies.key in url
assert 'pivot=%s' % tab in url
# Test a different base
base = 'https://doesnotexist.com/plex'
url = movies.getWebURL(base=base)
assert url.startswith(base)
def test_library_MovieSection_PlexWebURL_hub(plex, movies):
hubs = movies.hubs()
hub = next(iter(hubs), None)
assert hub is not None
url = hub.section().getWebURL(key=hub.key)
assert url.startswith('https://app.plex.tv/desktop')
assert plex.machineIdentifier in url
assert 'source=%s' % movies.key in url
assert quote_plus(hub.key) in url
def test_library_ShowSection_all(tvshows):
assert len(tvshows.all(title__iexact="The 100"))
def test_library_ShowSection_searchShows(tvshows):
assert tvshows.searchShows(title="The 100")
def test_library_ShowSection_searchSeasons(tvshows):
assert tvshows.searchSeasons(**{"show.title": "The 100"})
def test_library_ShowSection_searchEpisodes(tvshows):
assert tvshows.searchEpisodes(title="Winter Is Coming")
def test_library_ShowSection_recentlyAdded(tvshows, show):
season = show.season(1)
episode = season.episode(1)
assert show in tvshows.recentlyAdded()
assert show in tvshows.recentlyAddedShows()
assert season in tvshows.recentlyAddedSeasons()
assert episode in tvshows.recentlyAddedEpisodes()
def test_library_ShowSection_playlists(tvshows, show):
episodes = show.episodes()
try:
playlist = tvshows.createPlaylist("test_library_ShowSection_playlists", episodes[:3])
playlists = tvshows.playlists()
assert len(playlists)
assert playlist in playlists
p = tvshows.playlist(playlist.title)
assert playlist == p
playlists = tvshows.playlists(title="test_", sort="mediaCount:asc")
assert playlist in playlists
finally:
playlist.delete()
def test_library_ShowSection_playlist_exception(tvshows):
with pytest.raises(NotFound):
tvshows.playlist("Does Not Exists")
def test_library_MusicSection_albums(music):
assert len(music.albums())
def test_library_MusicSection_searchArtists(music):
assert len(music.searchArtists(title="Broke for Free"))
def test_library_MusicSection_searchAlbums(music):
assert len(music.searchAlbums(title="Layers"))
def test_library_MusicSection_searchTracks(music):
assert len(music.searchTracks(title="As Colourful As Ever"))
def test_library_MusicSection_recentlyAdded(music, artist):
album = artist.albums()[0]
track = album.tracks()[0]
assert artist in music.recentlyAdded()
assert artist in music.recentlyAddedArtists()
assert album in music.recentlyAddedAlbums()
assert track in music.recentlyAddedTracks()
def test_library_PhotoSection_searchAlbums(photos, photoalbum):
title = photoalbum.title
albums = photos.searchAlbums(title)
assert len(albums)
def test_library_PhotoSection_searchPhotos(photos, photoalbum):
title = photoalbum.photos()[0].title
assert len(photos.searchPhotos(title))
def test_library_PhotoSection_recentlyAdded(photos, photoalbum):
assert photoalbum in photos.recentlyAddedAlbums()
def test_library_and_section_search_for_movie(plex, movies):
find = "Elephants Dream"
l_search = plex.library.search(find)
s_search = movies.search(find)
assert l_search == s_search
def test_library_settings(movies):
settings = movies.settings()
assert len(settings) >= 1
def test_library_editAdvanced_default(movies):
movies.editAdvanced(hidden=2)
for setting in movies.settings():
if setting.id == "hidden":
assert int(setting.value) == 2
movies.editAdvanced(collectionMode=0)
for setting in movies.settings():
if setting.id == "collectionMode":
assert int(setting.value) == 0
movies.defaultAdvanced()
for setting in movies.settings():
assert str(setting.value) == str(setting.default)
def test_search_with_weird_a(plex, tvshows):
ep_title = "Coup de Grâce"
result_root = plex.search(ep_title)
result_shows = tvshows.searchEpisodes(title=ep_title)
assert result_root
assert result_shows
assert result_root == result_shows
def test_crazy_search(plex, movies, movie):
assert movie in movies.search(
actor=movie.actors[0], sort="titleSort"
), "Unable to search movie by actor."
assert movie in movies.search(
director=movie.directors[0]
), "Unable to search movie by director."
assert movie in movies.search(
year=["2006", "2007"]
), "Unable to search movie by year."
assert movie not in movies.search(year=2007), "Unable to filter movie by year."
assert movie in movies.search(actor=movie.actors[0].tag)
assert len(movies.search(container_start=2, maxresults=1)) == 1
assert len(movies.search(container_size=None)) == 4
assert len(movies.search(container_size=1)) == 4
assert len(movies.search(container_start=9999, container_size=1)) == 0
assert len(movies.search(container_start=2, container_size=1)) == 2
def test_library_section_timeline(plex, movies):
tl = movies.timeline()
assert tl.TAG == "LibraryTimeline"
assert tl.size > 0
assert tl.allowSync is False
assert tl.art == "/:/resources/movie-fanart.jpg"
assert tl.content == "secondary"
assert tl.identifier == "com.plexapp.plugins.library"
assert datetime.fromtimestamp(tl.latestEntryTime).date() == datetime.today().date()
assert tl.mediaTagPrefix == "/system/bundle/media/flags/"
assert tl.mediaTagVersion > 1
assert tl.thumb == "/:/resources/movie.png"
assert tl.title1 == "Movies"
assert utils.is_int(tl.updateQueueSize, gte=0)
assert tl.viewGroup == "secondary"
assert tl.viewMode == 65592
def test_library_MovieSection_hubSearch(movies):
assert movies.hubSearch("Elephants Dream")
def test_library_MovieSection_search(movies, movie, collection):
movie.addLabel("test_search")
movie.addCollection("test_search")
_test_library_search(movies, movie)
movie.removeLabel("test_search", locked=False)
movie.removeCollection("test_search", locked=False)
_test_library_search(movies, collection)
def test_library_MovieSection_search_FilterChoice(movies, collection):
filterChoice = next(c for c in movies.listFilterChoices("collection") if c.title == collection.title)
results = movies.search(filters={'collection': filterChoice})
movie = collection.items()[0]
assert movie in results
def test_library_MovieSection_advancedSearch(movies, movie):
advancedFilters = {
'and': [
{
'or': [
{'title': 'elephant'},
{'title': 'bunny'}
]
},
{'year>>': 1990},
{'unwatched': True}
]
}
results = movies.search(filters=advancedFilters)
assert movie in results
results = movies.search(limit=1)
assert len(results) == 1
def test_library_ShowSection_search(tvshows, show):
show.addLabel("test_search")
show.addCollection("test_search")
_test_library_search(tvshows, show)
show.removeLabel("test_search", locked=False)
show.removeCollection("test_search", locked=False)
season = show.season(season=1)
_test_library_search(tvshows, season)
episode = season.episode(episode=1)
_test_library_search(tvshows, episode)
# Additional test for mapping field to the correct libtype
assert tvshows.search(unwatched=True) # equal to episode.unwatched=True
def test_library_MusicSection_search(music, artist):
artist.addGenre("test_search")
artist.addStyle("test_search")
artist.addMood("test_search")
artist.addCollection("test_search")
_test_library_search(music, artist)
artist.removeGenre("test_search", locked=False)
artist.removeStyle("test_search", locked=False)
artist.removeMood("test_search", locked=False)
artist.removeCollection("test_search", locked=False)
album = artist.album("Layers")
album.addGenre("test_search")
album.addStyle("test_search")
album.addMood("test_search")
album.addCollection("test_search")
album.addLabel("test_search")
_test_library_search(music, album)
album.removeGenre("test_search", locked=False)
album.removeStyle("test_search", locked=False)
album.removeMood("test_search", locked=False)
album.removeCollection("test_search", locked=False)
album.removeLabel("test_search", locked=False)
track = album.track(track=1)
track.addMood("test_search")
_test_library_search(music, track)
track.removeMood("test_search", locked=False)
def test_library_PhotoSection_search(photos, photoalbum):
photo = photoalbum.photo("photo1")
photo.addTag("test_search")
_test_library_search(photos, photo)
photo.removeTag("test_search")
def test_library_MovieSection_search_sort(movies):
results = movies.search(sort="titleSort")
titleSort = [r.titleSort for r in results]
assert titleSort == sorted(titleSort)
results_asc = movies.search(sort="titleSort:asc")
titleSort_asc = [r.titleSort for r in results_asc]
assert titleSort == titleSort_asc
results_desc = movies.search(sort="titleSort:desc")
titleSort_desc = [r.titleSort for r in results_desc]
assert titleSort_desc == sorted(titleSort_desc, reverse=True)
# Test manually added sorts
results_guid = movies.search(sort="guid")
guid_asc = [r.guid for r in results_guid]
assert guid_asc == sorted(guid_asc)
results_summary = movies.search(sort="summary")
summary_asc = [r.summary for r in results_summary]
assert summary_asc == sorted(summary_asc)
results_tagline = movies.search(sort="tagline")
tagline_asc = [r.tagline for r in results_tagline if r.tagline]
assert tagline_asc == sorted(tagline_asc)
results_updatedAt = movies.search(sort="updatedAt")
updatedAt_asc = [r.updatedAt for r in results_updatedAt]
assert updatedAt_asc == sorted(updatedAt_asc)
# Test multi-sort
results_multi_str = movies.search(sort="year:asc,titleSort:asc")
titleSort_multi_str = [(r.year, r.titleSort) for r in results_multi_str]
assert titleSort_multi_str == sorted(titleSort_multi_str)
results_multi_list = movies.search(sort=["year:desc", "titleSort:desc"])
titleSort_multi_list = [(r.year, r.titleSort) for r in results_multi_list]
assert titleSort_multi_list == sorted(titleSort_multi_list, reverse=True)
# Test sort using FilteringSort object
sortObj = next(s for s in movies.listSorts() if s.key == "year")
results_sortObj = movies.search(sort=sortObj)
sortObj_list = [r.year for r in results_sortObj]
assert sortObj_list == sorted(sortObj_list, reverse=True)
def test_library_ShowSection_search_sort(tvshows):
# Test predefined Plex mult-sort
seasonAsc = "season.index,season.titleSort"
results = tvshows.search(sort=seasonAsc, libtype="season")
sortedResults = sorted(results, key=lambda s: (s.index, s.titleSort))
assert results == sortedResults
seasonShowAsc = "show.titleSort,index"
results = tvshows.search(sort=seasonShowAsc, libtype="season")
sortedResults = sorted(results, key=lambda s: (s.show().titleSort, s.index))
assert results == sortedResults
episodeShowAsc = (
"show.titleSort,season.index:nullsLast,episode.index:nullsLast,"
"episode.originallyAvailableAt:nullsLast,episode.titleSort,episode.id"
)
results = tvshows.search(sort=episodeShowAsc, libtype="episode")
sortedResults = sorted(
results,
key=lambda e: (
e.show().titleSort, e.season().index, e.index,
e.originallyAvailableAt, e.titleSort, e.ratingKey)
)
assert results == sortedResults
episodeShowDesc = (
"show.titleSort:desc,season.index:nullsLast,episode.index:nullsLast,"
"episode.originallyAvailableAt:nullsLast,episode.titleSort,episode.id"
)
results = tvshows.search(sort=episodeShowDesc, libtype="episode")
sortedResults = sorted(
sorted(
results,
key=lambda e: (
e.season().index, e.index,
e.originallyAvailableAt, e.titleSort, e.ratingKey)
),
key=lambda e: e.show().titleSort,
reverse=True
)
assert results == sortedResults
# Test manually added sorts
results_index = tvshows.search(sort="show.index,season.index,episode.index", libtype="episode")
index_asc = [(r.show().index, r.season().index, r.index) for r in results_index]
assert index_asc == sorted(index_asc)
def test_library_MusicSection_search_sort(music):
# Test predefined Plex mult-sort
albumArtistAsc = "artist.titleSort,album.titleSort,album.index,album.id,album.originallyAvailableAt"
results = music.search(sort=albumArtistAsc, libtype="album")
sortedResults = sorted(
results,
key=lambda a: (
a.artist().titleSort, a.titleSort, a.index, a.ratingKey, a.originallyAvailableAt
)
)
assert results == sortedResults
trackAlbumArtistAsc = (
"artist.titleSort,album.titleSort,album.year,"
"track.absoluteIndex,track.index,track.titleSort,track.id"
)
results = music.search(sort=trackAlbumArtistAsc, libtype="track")
sortedResults = sorted(
results,
key=lambda t: (
t.artist().titleSort, t.album().titleSort, t.album().year,
t.index, t.titleSort, t.ratingKey # Skip unknown absoluteIndex
)
)
assert results == sortedResults
def test_library_search_exceptions(movies):
with pytest.raises(BadRequest):
movies.listFilterChoices(field="123abc.title")
with pytest.raises(BadRequest):
movies.search(**{"123abc": True})
with pytest.raises(BadRequest):
movies.search(year="123abc")
with pytest.raises(BadRequest):
movies.search(sort="123abc")
with pytest.raises(BadRequest):
movies.search(filters=[])
with pytest.raises(BadRequest):
movies.search(filters={'and': {'title': 'test'}})
with pytest.raises(BadRequest):
movies.search(filters={'and': [], 'title': 'test'})
with pytest.raises(NotFound):
movies.getFilterType(libtype="show")
with pytest.raises(NotFound):
movies.getFieldType(fieldType="unknown")
with pytest.raises(NotFound):
movies.listFilterChoices(field="unknown")
with pytest.raises(NotFound):
movies.search(unknown="unknown")
with pytest.raises(NotFound):
movies.search(**{"title<>!=": "unknown"})
with pytest.raises(NotFound):
movies.search(sort="unknown")
with pytest.raises(NotFound):
movies.search(sort="titleSort:bad")
def _test_library_search(library, obj):
# Create & operator
AndOperator = namedtuple("AndOperator", ["key", "title"])
andOp = AndOperator("&=", "and")
fields = library.listFields(obj.type)
for field in fields:
fieldAttr = field.key.split(".")[-1]
operators = library.listOperators(field.type)
if field.type in {"tag", "string"}:
operators += [andOp]
for operator in operators:
if (
fieldAttr == "unmatched" and operator.key == "!="
or fieldAttr in {"audienceRating", "rating"} and operator.key in {"=", "!="}
or fieldAttr == "userRating"
):
continue
value = getattr(obj, fieldAttr, None)
if field.type == "boolean" and value is None:
value = fieldAttr.startswith("unwatched")
if field.type == "tag" and isinstance(value, list) and value and operator.title != "and":
value = value[0]
elif value is None:
continue
if operator.title == "begins with":
searchValue = value[:3]
elif operator.title == "ends with":
searchValue = value[-3:]
elif "contain" in operator.title:
searchValue = value.split(" ")[0]
elif operator.title == "is less than":
searchValue = value + 1
elif operator.title == "is greater than":
searchValue = max(value - 1, 1)
elif operator.title == "is before":
searchValue = value + timedelta(days=1)
elif operator.title == "is after":
searchValue = value - timedelta(days=1)
else:
searchValue = value
_do_test_library_search(library, obj, field, operator, searchValue)
# Test search again using string tag and date
if field.type == "tag" and fieldAttr != "contentRating":
if not isinstance(searchValue, list):
searchValue = [searchValue]
searchValue = [v.tag for v in searchValue]
_do_test_library_search(library, obj, field, operator, searchValue)
elif field.type == "date":
searchValue = searchValue.strftime("%Y-%m-%d")
_do_test_library_search(library, obj, field, operator, searchValue)
searchValue = "1s"
_do_test_library_search(library, obj, field, operator, searchValue)
def _do_test_library_search(library, obj, field, operator, searchValue):
searchFilter = {field.key + operator.key[:-1]: searchValue}
results = library.search(libtype=obj.type, filters=searchFilter)
if operator.key.startswith("!") or operator.key.startswith(">>") and (searchValue == 1 or searchValue == "1s"):
assert obj not in results
else:
assert obj in results