forked from pushingkarmaorg/python-plexapi
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlibrary.py
3327 lines (2701 loc) · 140 KB
/
library.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
from __future__ import annotations
import re
from typing import Any, TYPE_CHECKING
import warnings
from collections import defaultdict
from datetime import datetime
from functools import cached_property
from urllib.parse import parse_qs, quote_plus, urlencode, urlparse
from plexapi import log, media, utils
from plexapi.base import OPERATORS, PlexObject
from plexapi.exceptions import BadRequest, NotFound
from plexapi.mixins import (
MovieEditMixins, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins,
ArtistEditMixins, AlbumEditMixins, TrackEditMixins, PhotoalbumEditMixins, PhotoEditMixins
)
from plexapi.settings import Setting
from plexapi.utils import deprecated
if TYPE_CHECKING:
from plexapi.audio import Track
class Library(PlexObject):
""" Represents a PlexServer library. This contains all sections of media defined
in your Plex server including video, shows and audio.
Attributes:
key (str): '/library'
identifier (str): Unknown ('com.plexapp.plugins.library').
mediaTagVersion (str): Unknown (/system/bundle/media/flags/)
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to.
title1 (str): 'Plex Library' (not sure how useful this is).
title2 (str): Second title (this is blank on my setup).
"""
key = '/library'
def _loadData(self, data):
self._data = data
self.identifier = data.attrib.get('identifier')
self.mediaTagVersion = data.attrib.get('mediaTagVersion')
self.title1 = data.attrib.get('title1')
self.title2 = data.attrib.get('title2')
self._sectionsByID = {} # cached sections by key
self._sectionsByTitle = {} # cached sections by title
def _loadSections(self):
""" Loads and caches all the library sections. """
key = '/library/sections'
sectionsByID = {}
sectionsByTitle = defaultdict(list)
libcls = {
'movie': MovieSection,
'show': ShowSection,
'artist': MusicSection,
'photo': PhotoSection,
}
for elem in self._server.query(key):
section = libcls.get(elem.attrib.get('type'), LibrarySection)(self._server, elem, initpath=key)
sectionsByID[section.key] = section
sectionsByTitle[section.title.lower().strip()].append(section)
self._sectionsByID = sectionsByID
self._sectionsByTitle = dict(sectionsByTitle)
def sections(self):
""" Returns a list of all media sections in this library. Library sections may be any of
:class:`~plexapi.library.MovieSection`, :class:`~plexapi.library.ShowSection`,
:class:`~plexapi.library.MusicSection`, :class:`~plexapi.library.PhotoSection`.
"""
self._loadSections()
return list(self._sectionsByID.values())
def section(self, title):
""" Returns the :class:`~plexapi.library.LibrarySection` that matches the specified title.
Note: Multiple library sections with the same title is ambiguous.
Use :func:`~plexapi.library.Library.sectionByID` instead for an exact match.
Parameters:
title (str): Title of the section to return.
Raises:
:exc:`~plexapi.exceptions.NotFound`: The library section title is not found on the server.
"""
normalized_title = title.lower().strip()
if not self._sectionsByTitle or normalized_title not in self._sectionsByTitle:
self._loadSections()
try:
sections = self._sectionsByTitle[normalized_title]
except KeyError:
raise NotFound(f'Invalid library section: {title}') from None
if len(sections) > 1:
warnings.warn(
'Multiple library sections with the same title found, use "sectionByID" instead. '
'Returning the last section.'
)
return sections[-1]
def sectionByID(self, sectionID):
""" Returns the :class:`~plexapi.library.LibrarySection` that matches the specified sectionID.
Parameters:
sectionID (int): ID of the section to return.
Raises:
:exc:`~plexapi.exceptions.NotFound`: The library section ID is not found on the server.
"""
if not self._sectionsByID or sectionID not in self._sectionsByID:
self._loadSections()
try:
return self._sectionsByID[sectionID]
except KeyError:
raise NotFound(f'Invalid library sectionID: {sectionID}') from None
def hubs(self, sectionID=None, identifier=None, **kwargs):
""" Returns a list of :class:`~plexapi.library.Hub` across all library sections.
Parameters:
sectionID (int or str or list, optional):
IDs of the sections to limit results or "playlists".
identifier (str or list, optional):
Names of identifiers to limit results.
Available on `Hub` instances as the `hubIdentifier` attribute.
Examples: 'home.continue' or 'home.ondeck'
"""
if sectionID:
if not isinstance(sectionID, list):
sectionID = [sectionID]
kwargs['contentDirectoryID'] = ",".join(map(str, sectionID))
if identifier:
if not isinstance(identifier, list):
identifier = [identifier]
kwargs['identifier'] = ",".join(identifier)
key = f'/hubs{utils.joinArgs(kwargs)}'
return self.fetchItems(key)
def all(self, **kwargs):
""" Returns a list of all media from all library sections.
This may be a very large dataset to retrieve.
"""
items = []
for section in self.sections():
for item in section.all(**kwargs):
items.append(item)
return items
def onDeck(self):
""" Returns a list of all media items on deck. """
return self.fetchItems('/library/onDeck')
def recentlyAdded(self):
""" Returns a list of all media items recently added. """
return self.fetchItems('/library/recentlyAdded')
def search(self, title=None, libtype=None, **kwargs):
""" Searching within a library section is much more powerful. It seems certain
attributes on the media objects can be targeted to filter this search down
a bit, but I haven't found the documentation for it.
Example: "studio=Comedy%20Central" or "year=1999" "title=Kung Fu" all work. Other items
such as actor=<id> seem to work, but require you already know the id of the actor.
TLDR: This is untested but seems to work. Use library section search when you can.
"""
args = {}
if title:
args['title'] = title
if libtype:
args['type'] = utils.searchType(libtype)
for attr, value in kwargs.items():
args[attr] = value
key = f'/library/all{utils.joinArgs(args)}'
return self.fetchItems(key)
def cleanBundles(self):
""" Poster images and other metadata for items in your library are kept in "bundle"
packages. When you remove items from your library, these bundles aren't immediately
removed. Removing these old bundles can reduce the size of your install. By default, your
server will automatically clean up old bundles once a week as part of Scheduled Tasks.
"""
# TODO: Should this check the response for success or the correct mediaprefix?
self._server.query('/library/clean/bundles?async=1', method=self._server._session.put)
return self
def emptyTrash(self):
""" If a library has items in the Library Trash, use this option to empty the Trash. """
for section in self.sections():
section.emptyTrash()
return self
def optimize(self):
""" The Optimize option cleans up the server database from unused or fragmented data.
For example, if you have deleted or added an entire library or many items in a
library, you may like to optimize the database.
"""
self._server.query('/library/optimize?async=1', method=self._server._session.put)
return self
def update(self):
""" Scan this library for new items."""
self._server.query('/library/sections/all/refresh')
return self
def cancelUpdate(self):
""" Cancel a library update. """
key = '/library/sections/all/refresh'
self._server.query(key, method=self._server._session.delete)
return self
def refresh(self):
""" Forces a download of fresh media information from the internet.
This can take a long time. Any locked fields are not modified.
"""
self._server.query('/library/sections/all/refresh?force=1')
return self
def deleteMediaPreviews(self):
""" Delete the preview thumbnails for the all sections. This cannot be
undone. Recreating media preview files can take hours or even days.
"""
for section in self.sections():
section.deleteMediaPreviews()
return self
def add(self, name='', type='', agent='', scanner='', location='', language='en-US', *args, **kwargs):
""" Simplified add for the most common options.
Parameters:
name (str): Name of the library
agent (str): Example com.plexapp.agents.imdb
type (str): movie, show, # check me
location (str or list): /path/to/files, ["/path/to/files", "/path/to/morefiles"]
language (str): Four letter language code (e.g. en-US)
kwargs (dict): Advanced options should be passed as a dict. where the id is the key.
**Photo Preferences**
* **agent** (str): com.plexapp.agents.none
* **enableAutoPhotoTags** (bool): Tag photos. Default value false.
* **enableBIFGeneration** (bool): Enable video preview thumbnails. Default value true.
* **includeInGlobal** (bool): Include in dashboard. Default value true.
* **scanner** (str): Plex Photo Scanner
**Movie Preferences**
* **agent** (str): com.plexapp.agents.none, com.plexapp.agents.imdb, tv.plex.agents.movie,
com.plexapp.agents.themoviedb
* **enableBIFGeneration** (bool): Enable video preview thumbnails. Default value true.
* **enableCinemaTrailers** (bool): Enable Cinema Trailers. Default value true.
* **includeInGlobal** (bool): Include in dashboard. Default value true.
* **scanner** (str): Plex Movie, Plex Movie Scanner, Plex Video Files Scanner, Plex Video Files
**IMDB Movie Options** (com.plexapp.agents.imdb)
* **title** (bool): Localized titles. Default value false.
* **extras** (bool): Find trailers and extras automatically (Plex Pass required). Default value true.
* **only_trailers** (bool): Skip extras which aren't trailers. Default value false.
* **redband** (bool): Use red band (restricted audiences) trailers when available. Default value false.
* **native_subs** (bool): Include extras with subtitles in Library language. Default value false.
* **cast_list** (int): Cast List Source: Default value 1 Possible options: 0:IMDb,1:The Movie Database.
* **ratings** (int): Ratings Source, Default value 0 Possible options:
0:Rotten Tomatoes, 1:IMDb, 2:The Movie Database.
* **summary** (int): Plot Summary Source: Default value 1 Possible options: 0:IMDb,1:The Movie Database.
* **country** (int): Default value 46 Possible options 0:Argentina, 1:Australia, 2:Austria,
3:Belgium, 4:Belize, 5:Bolivia, 6:Brazil, 7:Canada, 8:Chile, 9:Colombia, 10:Costa Rica,
11:Czech Republic, 12:Denmark, 13:Dominican Republic, 14:Ecuador, 15:El Salvador,
16:France, 17:Germany, 18:Guatemala, 19:Honduras, 20:Hong Kong SAR, 21:Ireland,
22:Italy, 23:Jamaica, 24:Korea, 25:Liechtenstein, 26:Luxembourg, 27:Mexico, 28:Netherlands,
29:New Zealand, 30:Nicaragua, 31:Panama, 32:Paraguay, 33:Peru, 34:Portugal,
35:Peoples Republic of China, 36:Puerto Rico, 37:Russia, 38:Singapore, 39:South Africa,
40:Spain, 41:Sweden, 42:Switzerland, 43:Taiwan, 44:Trinidad, 45:United Kingdom,
46:United States, 47:Uruguay, 48:Venezuela.
* **collections** (bool): Use collection info from The Movie Database. Default value false.
* **localart** (bool): Prefer artwork based on library language. Default value true.
* **adult** (bool): Include adult content. Default value false.
* **usage** (bool): Send anonymous usage data to Plex. Default value true.
**TheMovieDB Movie Options** (com.plexapp.agents.themoviedb)
* **collections** (bool): Use collection info from The Movie Database. Default value false.
* **localart** (bool): Prefer artwork based on library language. Default value true.
* **adult** (bool): Include adult content. Default value false.
* **country** (int): Country (used for release date and content rating). Default value 47 Possible
options 0:, 1:Argentina, 2:Australia, 3:Austria, 4:Belgium, 5:Belize, 6:Bolivia, 7:Brazil, 8:Canada,
9:Chile, 10:Colombia, 11:Costa Rica, 12:Czech Republic, 13:Denmark, 14:Dominican Republic, 15:Ecuador,
16:El Salvador, 17:France, 18:Germany, 19:Guatemala, 20:Honduras, 21:Hong Kong SAR, 22:Ireland,
23:Italy, 24:Jamaica, 25:Korea, 26:Liechtenstein, 27:Luxembourg, 28:Mexico, 29:Netherlands,
30:New Zealand, 31:Nicaragua, 32:Panama, 33:Paraguay, 34:Peru, 35:Portugal,
36:Peoples Republic of China, 37:Puerto Rico, 38:Russia, 39:Singapore, 40:South Africa, 41:Spain,
42:Sweden, 43:Switzerland, 44:Taiwan, 45:Trinidad, 46:United Kingdom, 47:United States, 48:Uruguay,
49:Venezuela.
**Show Preferences**
* **agent** (str): com.plexapp.agents.none, com.plexapp.agents.thetvdb, com.plexapp.agents.themoviedb,
tv.plex.agents.series
* **enableBIFGeneration** (bool): Enable video preview thumbnails. Default value true.
* **episodeSort** (int): Episode order. Default -1 Possible options: 0:Oldest first, 1:Newest first.
* **flattenSeasons** (int): Seasons. Default value 0 Possible options: 0:Show,1:Hide.
* **includeInGlobal** (bool): Include in dashboard. Default value true.
* **scanner** (str): Plex TV Series, Plex Series Scanner
**TheTVDB Show Options** (com.plexapp.agents.thetvdb)
* **extras** (bool): Find trailers and extras automatically (Plex Pass required). Default value true.
* **native_subs** (bool): Include extras with subtitles in Library language. Default value false.
**TheMovieDB Show Options** (com.plexapp.agents.themoviedb)
* **collections** (bool): Use collection info from The Movie Database. Default value false.
* **localart** (bool): Prefer artwork based on library language. Default value true.
* **adult** (bool): Include adult content. Default value false.
* **country** (int): Country (used for release date and content rating). Default value 47 options
0:, 1:Argentina, 2:Australia, 3:Austria, 4:Belgium, 5:Belize, 6:Bolivia, 7:Brazil, 8:Canada, 9:Chile,
10:Colombia, 11:Costa Rica, 12:Czech Republic, 13:Denmark, 14:Dominican Republic, 15:Ecuador,
16:El Salvador, 17:France, 18:Germany, 19:Guatemala, 20:Honduras, 21:Hong Kong SAR, 22:Ireland,
23:Italy, 24:Jamaica, 25:Korea, 26:Liechtenstein, 27:Luxembourg, 28:Mexico, 29:Netherlands,
30:New Zealand, 31:Nicaragua, 32:Panama, 33:Paraguay, 34:Peru, 35:Portugal,
36:Peoples Republic of China, 37:Puerto Rico, 38:Russia, 39:Singapore, 40:South Africa,
41:Spain, 42:Sweden, 43:Switzerland, 44:Taiwan, 45:Trinidad, 46:United Kingdom, 47:United States,
48:Uruguay, 49:Venezuela.
**Other Video Preferences**
* **agent** (str): com.plexapp.agents.none, com.plexapp.agents.imdb, com.plexapp.agents.themoviedb
* **enableBIFGeneration** (bool): Enable video preview thumbnails. Default value true.
* **enableCinemaTrailers** (bool): Enable Cinema Trailers. Default value true.
* **includeInGlobal** (bool): Include in dashboard. Default value true.
* **scanner** (str): Plex Movie Scanner, Plex Video Files Scanner
**IMDB Other Video Options** (com.plexapp.agents.imdb)
* **title** (bool): Localized titles. Default value false.
* **extras** (bool): Find trailers and extras automatically (Plex Pass required). Default value true.
* **only_trailers** (bool): Skip extras which aren't trailers. Default value false.
* **redband** (bool): Use red band (restricted audiences) trailers when available. Default value false.
* **native_subs** (bool): Include extras with subtitles in Library language. Default value false.
* **cast_list** (int): Cast List Source: Default value 1 Possible options: 0:IMDb,1:The Movie Database.
* **ratings** (int): Ratings Source Default value 0 Possible options:
0:Rotten Tomatoes,1:IMDb,2:The Movie Database.
* **summary** (int): Plot Summary Source: Default value 1 Possible options: 0:IMDb,1:The Movie Database.
* **country** (int): Country: Default value 46 Possible options: 0:Argentina, 1:Australia, 2:Austria,
3:Belgium, 4:Belize, 5:Bolivia, 6:Brazil, 7:Canada, 8:Chile, 9:Colombia, 10:Costa Rica,
11:Czech Republic, 12:Denmark, 13:Dominican Republic, 14:Ecuador, 15:El Salvador, 16:France,
17:Germany, 18:Guatemala, 19:Honduras, 20:Hong Kong SAR, 21:Ireland, 22:Italy, 23:Jamaica,
24:Korea, 25:Liechtenstein, 26:Luxembourg, 27:Mexico, 28:Netherlands, 29:New Zealand, 30:Nicaragua,
31:Panama, 32:Paraguay, 33:Peru, 34:Portugal, 35:Peoples Republic of China, 36:Puerto Rico,
37:Russia, 38:Singapore, 39:South Africa, 40:Spain, 41:Sweden, 42:Switzerland, 43:Taiwan, 44:Trinidad,
45:United Kingdom, 46:United States, 47:Uruguay, 48:Venezuela.
* **collections** (bool): Use collection info from The Movie Database. Default value false.
* **localart** (bool): Prefer artwork based on library language. Default value true.
* **adult** (bool): Include adult content. Default value false.
* **usage** (bool): Send anonymous usage data to Plex. Default value true.
**TheMovieDB Other Video Options** (com.plexapp.agents.themoviedb)
* **collections** (bool): Use collection info from The Movie Database. Default value false.
* **localart** (bool): Prefer artwork based on library language. Default value true.
* **adult** (bool): Include adult content. Default value false.
* **country** (int): Country (used for release date and content rating). Default
value 47 Possible options 0:, 1:Argentina, 2:Australia, 3:Austria, 4:Belgium, 5:Belize,
6:Bolivia, 7:Brazil, 8:Canada, 9:Chile, 10:Colombia, 11:Costa Rica, 12:Czech Republic,
13:Denmark, 14:Dominican Republic, 15:Ecuador, 16:El Salvador, 17:France, 18:Germany,
19:Guatemala, 20:Honduras, 21:Hong Kong SAR, 22:Ireland, 23:Italy, 24:Jamaica,
25:Korea, 26:Liechtenstein, 27:Luxembourg, 28:Mexico, 29:Netherlands, 30:New Zealand,
31:Nicaragua, 32:Panama, 33:Paraguay, 34:Peru, 35:Portugal,
36:Peoples Republic of China, 37:Puerto Rico, 38:Russia, 39:Singapore,
40:South Africa, 41:Spain, 42:Sweden, 43:Switzerland, 44:Taiwan, 45:Trinidad,
46:United Kingdom, 47:United States, 48:Uruguay, 49:Venezuela.
"""
if isinstance(location, str):
location = [location]
locations = []
for path in location:
if not self._server.isBrowsable(path):
raise BadRequest(f'Path: {path} does not exist.')
locations.append(('location', path))
part = (f'/library/sections?name={quote_plus(name)}&type={type}&agent={agent}'
f'&scanner={quote_plus(scanner)}&language={language}&{urlencode(locations, doseq=True)}')
if kwargs:
prefs_params = {f'prefs[{k}]': v for k, v in kwargs.items()}
part += f'&{urlencode(prefs_params)}'
return self._server.query(part, method=self._server._session.post)
def history(self, maxresults=None, mindate=None):
""" Get Play History for all library Sections for the owner.
Parameters:
maxresults (int): Only return the specified number of results (optional).
mindate (datetime): Min datetime to return results from.
"""
hist = []
for section in self.sections():
hist.extend(section.history(maxresults=maxresults, mindate=mindate))
return hist
def tags(self, tag):
""" Returns a list of :class:`~plexapi.library.LibraryMediaTag` objects for the specified tag.
Parameters:
tag (str): Tag name (see :data:`~plexapi.utils.TAGTYPES`).
"""
tagType = utils.tagType(tag)
data = self._server.query(f'/library/tags?type={tagType}')
return self.findItems(data)
class LibrarySection(PlexObject):
""" Base class for a single library section.
Attributes:
agent (str): The metadata agent used for the library section (com.plexapp.agents.imdb, etc).
allowSync (bool): True if you allow syncing content from the library section.
art (str): Background artwork used to respresent the library section.
composite (str): Composite image used to represent the library section.
createdAt (datetime): Datetime the library section was created.
filters (bool): True if filters are available for the library section.
key (int): Key (or ID) of this library section.
language (str): Language represented in this section (en, xn, etc).
locations (List<str>): List of folder paths added to the library section.
refreshing (bool): True if this section is currently being refreshed.
scanner (str): Internal scanner used to find media (Plex Movie Scanner, Plex Premium Music Scanner, etc.)
thumb (str): Thumbnail image used to represent the library section.
title (str): Name of the library section.
type (str): Type of content section represents (movie, show, artist, photo).
updatedAt (datetime): Datetime the library section was last updated.
uuid (str): Unique id for the section (32258d7c-3e6c-4ac5-98ad-bad7a3b78c63)
"""
def _loadData(self, data):
self._data = data
self.agent = data.attrib.get('agent')
self.allowSync = utils.cast(bool, data.attrib.get('allowSync'))
self.art = data.attrib.get('art')
self.composite = data.attrib.get('composite')
self.createdAt = utils.toDatetime(data.attrib.get('createdAt'))
self.filters = utils.cast(bool, data.attrib.get('filters'))
self.key = utils.cast(int, data.attrib.get('key'))
self.language = data.attrib.get('language')
self.locations = self.listAttrs(data, 'path', etag='Location')
self.refreshing = utils.cast(bool, data.attrib.get('refreshing'))
self.scanner = data.attrib.get('scanner')
self.thumb = data.attrib.get('thumb')
self.title = data.attrib.get('title')
self.type = data.attrib.get('type')
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
self.uuid = data.attrib.get('uuid')
# Private attrs as we don't want a reload.
self._filterTypes = None
self._fieldTypes = None
self._totalViewSize = None
self._totalDuration = None
self._totalStorage = None
@cached_property
def totalSize(self):
""" Returns the total number of items in the library for the default library type. """
return self.totalViewSize(includeCollections=False)
@property
def totalDuration(self):
""" Returns the total duration (in milliseconds) of items in the library. """
if self._totalDuration is None:
self._getTotalDurationStorage()
return self._totalDuration
@property
def totalStorage(self):
""" Returns the total storage (in bytes) of items in the library. """
if self._totalStorage is None:
self._getTotalDurationStorage()
return self._totalStorage
def __getattribute__(self, attr):
# Intercept to call EditFieldMixin and EditTagMixin methods
# based on the item type being batch multi-edited
value = super().__getattribute__(attr)
if attr.startswith('_'): return value
if callable(value) and 'Mixin' in value.__qualname__:
if not isinstance(self._edits, dict):
raise AttributeError("Must enable batchMultiEdit() to use this method")
elif not hasattr(self._edits['items'][0], attr):
raise AttributeError(
f"Batch multi-editing '{self._edits['items'][0].__class__.__name__}' object has no attribute '{attr}'"
)
return value
def _getTotalDurationStorage(self):
""" Queries the Plex server for the total library duration and storage and caches the values. """
data = self._server.query('/media/providers?includeStorage=1')
xpath = (
'./MediaProvider[@identifier="com.plexapp.plugins.library"]'
'/Feature[@type="content"]'
f'/Directory[@id="{self.key}"]'
)
directory = next(iter(data.findall(xpath)), None)
if directory:
self._totalDuration = utils.cast(int, directory.attrib.get('durationTotal'))
self._totalStorage = utils.cast(int, directory.attrib.get('storageTotal'))
def totalViewSize(self, libtype=None, includeCollections=True):
""" Returns the total number of items in the library for a specified libtype.
The number of items for the default library type will be returned if no libtype is specified.
(e.g. Specify ``libtype='episode'`` for the total number of episodes
or ``libtype='albums'`` for the total number of albums.)
Parameters:
libtype (str, optional): The type of items to return the total number for (movie, show, season, episode,
artist, album, track, photoalbum). Default is the main library type.
includeCollections (bool, optional): True or False to include collections in the total number.
Default is True.
"""
args = {
'includeCollections': int(bool(includeCollections)),
'X-Plex-Container-Start': 0,
'X-Plex-Container-Size': 0
}
if libtype is not None:
if libtype == 'photo':
args['clusterZoomLevel'] = 1
else:
args['type'] = utils.searchType(libtype)
part = f'/library/sections/{self.key}/all{utils.joinArgs(args)}'
data = self._server.query(part)
return utils.cast(int, data.attrib.get("totalSize"))
def delete(self):
""" Delete a library section. """
try:
return self._server.query(f'/library/sections/{self.key}', method=self._server._session.delete)
except BadRequest: # pragma: no cover
msg = f'Failed to delete library {self.key}'
msg += 'You may need to allow this permission in your Plex settings.'
log.error(msg)
raise
def reload(self):
""" Reload the data for the library section. """
self._server.library._loadSections()
newLibrary = self._server.library.sectionByID(self.key)
self.__dict__.update(newLibrary.__dict__)
return self
def edit(self, agent=None, **kwargs):
""" Edit a library. See :class:`~plexapi.library.Library` for example usage.
Parameters:
agent (str, optional): The library agent.
kwargs (dict): Dict of settings to edit.
"""
if not agent:
agent = self.agent
locations = []
if kwargs.get('location'):
if isinstance(kwargs['location'], str):
kwargs['location'] = [kwargs['location']]
for path in kwargs.pop('location'):
if not self._server.isBrowsable(path):
raise BadRequest(f'Path: {path} does not exist.')
locations.append(('location', path))
params = list(kwargs.items()) + locations
part = f'/library/sections/{self.key}?agent={agent}&{urlencode(params, doseq=True)}'
self._server.query(part, method=self._server._session.put)
return self
def addLocations(self, location):
""" Add a location to a library.
Parameters:
location (str or list): A single folder path, list of paths.
Example:
.. code-block:: python
LibrarySection.addLocations('/path/1')
LibrarySection.addLocations(['/path/1', 'path/2', '/path/3'])
"""
locations = self.locations
if isinstance(location, str):
location = [location]
for path in location:
if not self._server.isBrowsable(path):
raise BadRequest(f'Path: {path} does not exist.')
locations.append(path)
return self.edit(location=locations)
def removeLocations(self, location):
""" Remove a location from a library.
Parameters:
location (str or list): A single folder path, list of paths.
Example:
.. code-block:: python
LibrarySection.removeLocations('/path/1')
LibrarySection.removeLocations(['/path/1', 'path/2', '/path/3'])
"""
locations = self.locations
if isinstance(location, str):
location = [location]
for path in location:
if path in locations:
locations.remove(path)
else:
raise BadRequest(f'Path: {location} does not exist in the library.')
if len(locations) == 0:
raise BadRequest('You are unable to remove all locations from a library.')
return self.edit(location=locations)
def get(self, title, **kwargs):
""" Returns the media item with the specified title and kwargs.
Parameters:
title (str): Title of the item to return.
kwargs (dict): Additional search parameters.
See :func:`~plexapi.library.LibrarySection.search` for more info.
Raises:
:exc:`~plexapi.exceptions.NotFound`: The title is not found in the library.
"""
try:
return self.search(title, limit=1, **kwargs)[0]
except IndexError:
msg = f"Unable to find item with title '{title}'"
if kwargs:
msg += f" and kwargs {kwargs}"
raise NotFound(msg) from None
def getGuid(self, guid):
""" Returns the media item with the specified external Plex, IMDB, TMDB, or TVDB ID.
Note: Only available for the Plex Movie and Plex TV Series agents.
Parameters:
guid (str): The external guid of the item to return.
Examples: Plex ``plex://show/5d9c086c46115600200aa2fe``
IMDB ``imdb://tt0944947``, TMDB ``tmdb://1399``, TVDB ``tvdb://121361``.
Raises:
:exc:`~plexapi.exceptions.NotFound`: The guid is not found in the library.
Example:
.. code-block:: python
result1 = library.getGuid('plex://show/5d9c086c46115600200aa2fe')
result2 = library.getGuid('imdb://tt0944947')
result3 = library.getGuid('tmdb://1399')
result4 = library.getGuid('tvdb://121361')
# Alternatively, create your own guid lookup dictionary for faster performance
guidLookup = {}
for item in library.all():
guidLookup[item.guid] = item
guidLookup.update({guid.id: item for guid in item.guids})
result1 = guidLookup['plex://show/5d9c086c46115600200aa2fe']
result2 = guidLookup['imdb://tt0944947']
result3 = guidLookup['tmdb://1399']
result4 = guidLookup['tvdb://121361']
"""
try:
if guid.startswith('plex://'):
result = self.search(guid=guid)[0]
return result
else:
dummy = self.search(maxresults=1)[0]
match = dummy.matches(agent=self.agent, title=guid.replace('://', '-'))
return self.search(guid=match[0].guid)[0]
except IndexError:
raise NotFound(f"Guid '{guid}' is not found in the library") from None
def all(self, libtype=None, **kwargs):
""" Returns a list of all items from this library section.
See description of :func:`~plexapi.library.LibrarySection.search()` for details about filtering / sorting.
"""
libtype = libtype or self.TYPE
return self.search(libtype=libtype, **kwargs)
def folders(self):
""" Returns a list of available :class:`~plexapi.library.Folder` for this library section.
"""
key = f'/library/sections/{self.key}/folder'
return self.fetchItems(key, Folder)
def managedHubs(self):
""" Returns a list of available :class:`~plexapi.library.ManagedHub` for this library section.
"""
key = f'/hubs/sections/{self.key}/manage'
return self.fetchItems(key, ManagedHub)
def resetManagedHubs(self):
""" Reset the managed hub customizations for this library section.
"""
key = f'/hubs/sections/{self.key}/manage'
self._server.query(key, method=self._server._session.delete)
def hubs(self):
""" Returns a list of available :class:`~plexapi.library.Hub` for this library section.
"""
key = f'/hubs/sections/{self.key}?includeStations=1'
return self.fetchItems(key)
def agents(self):
""" Returns a list of available :class:`~plexapi.media.Agent` for this library section.
"""
return self._server.agents(self.type)
def settings(self):
""" Returns a list of all library settings. """
key = f'/library/sections/{self.key}/prefs'
data = self._server.query(key)
return self.findItems(data, cls=Setting)
def editAdvanced(self, **kwargs):
""" Edit a library's advanced settings. """
data = {}
idEnums = {}
key = 'prefs[{}]'
for setting in self.settings():
if setting.type != 'bool':
idEnums[setting.id] = setting.enumValues
else:
idEnums[setting.id] = {0: False, 1: True}
for settingID, value in kwargs.items():
try:
enums = idEnums[settingID]
except KeyError:
raise NotFound(f'{value} not found in {list(idEnums.keys())}')
if value in enums:
data[key.format(settingID)] = value
else:
raise NotFound(f'{value} not found in {enums}')
return self.edit(**data)
def defaultAdvanced(self):
""" Edit all of library's advanced settings to default. """
data = {}
key = 'prefs[{}]'
for setting in self.settings():
if setting.type == 'bool':
data[key.format(setting.id)] = int(setting.default)
else:
data[key.format(setting.id)] = setting.default
return self.edit(**data)
def _lockUnlockAllField(self, field, libtype=None, locked=True):
""" Lock or unlock a field for all items in the library. """
libtype = libtype or self.TYPE
args = {
'type': utils.searchType(libtype),
f'{field}.locked': int(locked)
}
key = f'/library/sections/{self.key}/all{utils.joinArgs(args)}'
self._server.query(key, method=self._server._session.put)
return self
def lockAllField(self, field, libtype=None):
""" Lock a field for all items in the library.
Parameters:
field (str): The field to lock (e.g. thumb, rating, collection).
libtype (str, optional): The library type to lock (movie, show, season, episode,
artist, album, track, photoalbum, photo). Default is the main library type.
"""
return self._lockUnlockAllField(field, libtype=libtype, locked=True)
def unlockAllField(self, field, libtype=None):
""" Unlock a field for all items in the library.
Parameters:
field (str): The field to unlock (e.g. thumb, rating, collection).
libtype (str, optional): The library type to lock (movie, show, season, episode,
artist, album, track, photoalbum, photo). Default is the main library type.
"""
return self._lockUnlockAllField(field, libtype=libtype, locked=False)
def timeline(self):
""" Returns a timeline query for this library section. """
key = f'/library/sections/{self.key}/timeline'
data = self._server.query(key)
return LibraryTimeline(self, data)
def onDeck(self):
""" Returns a list of media items on deck from this library section. """
key = f'/library/sections/{self.key}/onDeck'
return self.fetchItems(key)
def continueWatching(self):
""" Return a list of media items in the library's Continue Watching hub. """
key = f'/hubs/sections/{self.key}/continueWatching/items'
return self.fetchItems(key)
def recentlyAdded(self, maxresults=50, libtype=None):
""" Returns a list of media items recently added from this library section.
Parameters:
maxresults (int): Max number of items to return (default 50).
libtype (str, optional): The library type to filter (movie, show, season, episode,
artist, album, track, photoalbum, photo). Default is the main library type.
"""
libtype = libtype or self.TYPE
return self.search(sort='addedAt:desc', maxresults=maxresults, libtype=libtype)
def firstCharacter(self):
key = f'/library/sections/{self.key}/firstCharacter'
return self.fetchItems(key, cls=FirstCharacter)
def analyze(self):
""" Run an analysis on all of the items in this library section. See
See :func:`~plexapi.base.PlexPartialObject.analyze` for more details.
"""
key = f'/library/sections/{self.key}/analyze'
self._server.query(key, method=self._server._session.put)
return self
def emptyTrash(self):
""" If a section has items in the Trash, use this option to empty the Trash. """
key = f'/library/sections/{self.key}/emptyTrash'
self._server.query(key, method=self._server._session.put)
return self
def update(self, path=None):
""" Scan this section for new media.
Parameters:
path (str, optional): Full path to folder to scan.
"""
key = f'/library/sections/{self.key}/refresh'
if path is not None:
key += f'?path={quote_plus(path)}'
self._server.query(key)
return self
def cancelUpdate(self):
""" Cancel update of this Library Section. """
key = f'/library/sections/{self.key}/refresh'
self._server.query(key, method=self._server._session.delete)
return self
def refresh(self):
""" Forces a download of fresh media information from the internet.
This can take a long time. Any locked fields are not modified.
"""
key = f'/library/sections/{self.key}/refresh?force=1'
self._server.query(key)
return self
def deleteMediaPreviews(self):
""" Delete the preview thumbnails for items in this library. This cannot
be undone. Recreating media preview files can take hours or even days.
"""
key = f'/library/sections/{self.key}/indexes'
self._server.query(key, method=self._server._session.delete)
return self
def _loadFilters(self):
""" Retrieves and caches the list of :class:`~plexapi.library.FilteringType` and
list of :class:`~plexapi.library.FilteringFieldType` for this library section.
"""
_key = ('/library/sections/{key}/{filter}?includeMeta=1&includeAdvanced=1'
'&X-Plex-Container-Start=0&X-Plex-Container-Size=0')
key = _key.format(key=self.key, filter='all')
data = self._server.query(key)
self._filterTypes = self.findItems(data, FilteringType, rtag='Meta')
self._fieldTypes = self.findItems(data, FilteringFieldType, rtag='Meta')
if self.TYPE != 'photo': # No collections for photo library
key = _key.format(key=self.key, filter='collections')
data = self._server.query(key)
self._filterTypes.extend(self.findItems(data, FilteringType, rtag='Meta'))
# Manually add guid field type, only allowing "is" operator
guidFieldType = '<FieldType type="guid"><Operator key="=" title="is"/></FieldType>'
self._fieldTypes.append(self._manuallyLoadXML(guidFieldType, FilteringFieldType))
def filterTypes(self):
""" Returns a list of available :class:`~plexapi.library.FilteringType` for this library section. """
if self._filterTypes is None:
self._loadFilters()
return self._filterTypes
def getFilterType(self, libtype=None):
""" Returns a :class:`~plexapi.library.FilteringType` for a specified libtype.
Parameters:
libtype (str, optional): The library type to filter (movie, show, season, episode,
artist, album, track, photoalbum, photo, collection).
Raises:
:exc:`~plexapi.exceptions.NotFound`: Unknown libtype for this library.
"""
libtype = libtype or self.TYPE
try:
return next(f for f in self.filterTypes() if f.type == libtype)
except StopIteration:
availableLibtypes = [f.type for f in self.filterTypes()]
raise NotFound(f'Unknown libtype "{libtype}" for this library. '
f'Available libtypes: {availableLibtypes}') from None
def fieldTypes(self):
""" Returns a list of available :class:`~plexapi.library.FilteringFieldType` for this library section. """
if self._fieldTypes is None:
self._loadFilters()
return self._fieldTypes
def getFieldType(self, fieldType):
""" Returns a :class:`~plexapi.library.FilteringFieldType` for a specified fieldType.
Parameters:
fieldType (str): The data type for the field (tag, integer, string, boolean, date,
subtitleLanguage, audioLanguage, resolution).
Raises:
:exc:`~plexapi.exceptions.NotFound`: Unknown fieldType for this library.
"""
try:
return next(f for f in self.fieldTypes() if f.type == fieldType)
except StopIteration:
availableFieldTypes = [f.type for f in self.fieldTypes()]
raise NotFound(f'Unknown field type "{fieldType}" for this library. '
f'Available field types: {availableFieldTypes}') from None
def listFilters(self, libtype=None):
""" Returns a list of available :class:`~plexapi.library.FilteringFilter` for a specified libtype.
This is the list of options in the filter dropdown menu
(`screenshot <../_static/images/LibrarySection.listFilters.png>`__).
Parameters:
libtype (str, optional): The library type to filter (movie, show, season, episode,
artist, album, track, photoalbum, photo, collection).
Example:
.. code-block:: python
availableFilters = [f.filter for f in library.listFilters()]
print("Available filter fields:", availableFilters)
"""
return self.getFilterType(libtype).filters
def listSorts(self, libtype=None):
""" Returns a list of available :class:`~plexapi.library.FilteringSort` for a specified libtype.
This is the list of options in the sorting dropdown menu
(`screenshot <../_static/images/LibrarySection.listSorts.png>`__).
Parameters:
libtype (str, optional): The library type to filter (movie, show, season, episode,
artist, album, track, photoalbum, photo, collection).
Example:
.. code-block:: python
availableSorts = [f.key for f in library.listSorts()]
print("Available sort fields:", availableSorts)
"""
return self.getFilterType(libtype).sorts
def listFields(self, libtype=None):
""" Returns a list of available :class:`~plexapi.library.FilteringFields` for a specified libtype.
This is the list of options in the custom filter dropdown menu
(`screenshot <../_static/images/LibrarySection.search.png>`__).
Parameters:
libtype (str, optional): The library type to filter (movie, show, season, episode,
artist, album, track, photoalbum, photo, collection).
Example:
.. code-block:: python
availableFields = [f.key.split('.')[-1] for f in library.listFields()]
print("Available fields:", availableFields)
"""
return self.getFilterType(libtype).fields
def listOperators(self, fieldType):
""" Returns a list of available :class:`~plexapi.library.FilteringOperator` for a specified fieldType.