forked from terrelsa13/MUMC
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmedia_cleaner.py
6492 lines (5557 loc) · 386 KB
/
media_cleaner.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
#!/usr/bin/env python3
import urllib.request as request
import json, urllib
import traceback
#import hashlib
import time
import uuid
#import sys
import os
from dateutil.parser import parse
from collections import defaultdict
from datetime import datetime,date,timedelta,timezone
from media_cleaner_config_defaults import get_default_config_values
def get_script_version():
Version='2.1.7'
return(Version)
def convert2json(rawjson):
#return a formatted string of the python JSON object
ezjson = json.dumps(rawjson, sort_keys=False, indent=4)
return(ezjson)
def print2json(rawjson):
#create a formatted string of the python JSON object
ezjson = convert2json(rawjson)
print(ezjson)
#Check if json index exists
def does_index_exist(item, indexvalue):
try:
exists = item[indexvalue]
except IndexError:
if (cfg.DEBUG):
print(str(indexvalue) + 'does not exist in item')
return(False)
if (cfg.DEBUG):
print(str(indexvalue) + 'does exist in item')
return(True)
#send url request
def requestURL(url, debugBool, reqeustDebugMessage, retries):
if (debugBool):
#DEBUG
print(reqeustDebugMessage + ' - url request')
print(url)
#first delay if needed
#delay value doubles each time the same API request is resent
delay = 1
#number of times after the intial API request to retry if an exception occurs
retryAttempts = int(retries)
getdata = True
#try sending url request specified number of times
#starting with a 1 second delay if an exception occurs and doubling the delay each attempt
while(getdata):
try:
with request.urlopen(url) as response:
if response.getcode() == 200:
try:
source = response.read()
data = json.loads(source)
getdata = False
if (debugBool):
#DEBUG
print(reqeustDebugMessage + ' - data')
print2json(data)
#return(data)
except Exception as err:
if (err.msg == 'Unauthorized'):
print('\n' + str(err))
raise RuntimeError('\nAUTH_ERROR: User Not Authorized To Access Library')
else:
time.sleep(delay)
#delay value doubles each time the same API request is resent
delay += delay
if (delay >= (2**retryAttempts)):
print('An error occured, a maximum of ' + str(retryAttempts) + ' attempts met, and no data retrieved from the \"' + reqeustDebugMessage + '\" lookup.')
return(err)
else:
getdata = False
print('An error occurred while attempting to retrieve data from the API.')
return('Attempt to get data at: ' + reqeustDebugMessage + '. Server responded with code: ' + str(response.getcode()))
except Exception as err:
if (err.msg == 'Unauthorized'):
print('\n' + str(err))
raise RuntimeError('\nAUTH_ERROR: User Not Authorized To Access Library')
else:
time.sleep(delay)
#delay value doubles each time the same API request is resent
delay += delay
if (delay >= (2**retryAttempts)):
print('An error occured, a maximum of ' + str(retryAttempts) + ' attempts met, and no data retrieved from the \"' + reqeustDebugMessage + '\" lookup.')
return(err)
return(data)
#Limit the amount of data returned for a single API call
def api_query_handler(url,StartIndex,TotalItems,QueryLimit,APIDebugMsg):
data=requestURL(url, cfg.DEBUG, APIDebugMsg, cfg.api_query_attempts)
TotalItems = data['TotalRecordCount']
StartIndex = StartIndex + QueryLimit
QueryLimit = cfg.api_query_item_limit
if ((StartIndex + QueryLimit) >= (TotalItems)):
QueryLimit = TotalItems - StartIndex
QueryItemsRemaining=False
if (QueryLimit > 0):
QueryItemsRemaining=True
if (cfg.DEBUG):
#DEBUG
print(APIDebugMsg + ' - API query handler')
print(url)
print('Starting at record index ' + str(StartIndex))
print('Asking for ' + str(QueryLimit) + ' records')
print('Total records for this query is ' + str(TotalItems))
print('There are records remaining: ' + str(QueryItemsRemaining))
return(data,StartIndex,TotalItems,QueryLimit,QueryItemsRemaining)
#emby or jellyfin?
def get_brand():
defaultbrand='emby'
print('0:emby\n1:jellyfin')
brand=input('Enter number for server branding (default ' + defaultbrand + '): ')
if (brand == ''):
return(defaultbrand)
elif (brand == '0'):
return(defaultbrand)
elif (brand == '1'):
return('jellyfin')
else:
print('Invalid choice. Default to emby.')
return(defaultbrand)
#ip address or url?
def get_url():
defaulturl='http://localhost'
url=input('Enter server ip or name (default ' + defaulturl + '): ')
if (url == ''):
return(defaulturl)
else:
if (url.find('://',3,8) >= 0):
return(url)
else:
url='http://' + url
print('Assuming server ip or name is: ' + url)
return(url)
#http or https port?
def get_port():
defaultport='8096'
valid_port=False
while (valid_port == False):
print('If you have not explicity changed this option, press enter for default.')
print('Space for no port.')
port=input('Enter port (default ' + defaultport + '): ')
if (port == ''):
valid_port=True
return(defaultport)
elif (port == ' '):
valid_port=True
return('')
else:
try:
port_float=float(port)
if ((port_float % 1) == 0):
port_int=int(port_float)
if ((int(port_int) >= 1) and (int(port_int) <= 65535)):
valid_port=True
return(str(port_int))
else:
print('\nInvalid port. Try again.\n')
else:
print('\nInvalid port. Try again.\n')
except:
print('\nInvalid port. Try again.\n')
#base url?
def get_base(brand):
defaultbase='emby'
#print('If you are using emby press enter for default.')
if (brand == defaultbase):
print('Using "/' + defaultbase + '" as base url')
return(defaultbase)
else:
print('If you have not explicity changed this option in jellyfin, press enter for default.')
print('For example: http://example.com/<baseurl>')
base=input('Enter base url (default /' + defaultbase + '): ')
if (base == ''):
return(defaultbase)
else:
if (base.find('/',0,1) == 0):
return(base[1:len(base)])
else:
return(base)
#admin username?
def get_admin_username():
return(input('Enter admin username: '))
#admin password?
def get_admin_password():
#print('Plain text password used to grab authentication key; hashed password stored in config file.')
print('Plain text password used to grab authentication key; password not stored.')
password=input('Enter admin password: ')
return(password)
#Blacklisting or Whitelisting?
def get_library_setup_behavior(library_setup_behavior):
defaultbehavior='blacklist'
valid_behavior=False
while (valid_behavior == False):
print('Decide how the script will use the libraries chosen for each user.')
print('0 - blacklist - Chosen libraries will blacklisted.')
print(' All other libraries will be whitelisted.')
print('1 - whitelist - Chosen libraries will whitelisted.')
print(' All other libraries will be blacklisted.')
if (library_setup_behavior == 'blacklist'):
print('')
print('Script previously setup using \'0 - ' + library_setup_behavior + '\'.')
elif (library_setup_behavior == 'whitelist'):
print('')
print('Script previously setup using \'1 - ' + library_setup_behavior + '\'.')
behavior=input('Choose how the script will use the chosen libraries. (default ' + defaultbehavior + '): ')
if (behavior == ''):
valid_behavior=True
return(defaultbehavior)
elif (behavior == '0'):
valid_behavior=True
return(defaultbehavior)
elif (behavior == '1'):
valid_behavior=True
return('whitelist')
else:
print('\nInvalid choice. Try again.\n')
#Blacklisting or Whitelisting?
def get_library_matching_behavior(library_matching_behavior):
defaultbehavior='byId'
valid_behavior=False
while (valid_behavior == False):
print('Decide how the script will match media items to libraries.')
print('0 - byId - Media items will be matched to libraries using \'LibraryIds\'.')
print('1 - byPath - Media items will be matched to libraries using \'Paths\'.')
print('2 - byNetworkPath - Media items will be matched to libraries using \'NetworkPaths\'.')
if ((library_matching_behavior == 'byId') or (library_matching_behavior == 'byPath') or (library_matching_behavior == 'byNetworkPath')):
print('')
print('Script previously setup to match media items to libraries ' + library_matching_behavior + '.')
behavior=input('Choose how the script will match media items to libraries. (default ' + defaultbehavior + '): ')
if (behavior == ''):
valid_behavior=True
return(defaultbehavior)
elif (behavior == '0'):
valid_behavior=True
return(defaultbehavior)
elif (behavior == '1'):
valid_behavior=True
return('byPath')
elif (behavior == '2'):
valid_behavior=True
return('byNetworkPath')
else:
print('\nInvalid choice. Try again.\n')
#Blacktagging or Whitetagging String Name?
def get_tag_name(tagbehavior,existingtag):
defaulttag=get_default_config_values(tagbehavior)
valid_tag=False
while (valid_tag == False):
print('Enter the desired ' + tagbehavior + ' name. Do not use backslash \'\\\'.')
print('Use a comma \',\' to seperate multiple tag names.')
print(' Ex: tagname,tag name,tag-name')
if (defaulttag == ''):
print('Leave blank to disable the ' + tagbehavior + 'ging functionality.')
inputtagname=input('Input desired ' + tagbehavior + 's: ')
else:
inputtagname=input('Input desired ' + tagbehavior + 's (default \'' + defaulttag + '\'): ')
#Remove duplicates
inputtagname = ','.join(set(inputtagname.split(',')))
if (inputtagname == ''):
valid_tag=True
return(defaulttag)
else:
if (inputtagname.find('\\') <= 0):
#replace single quote (') with backslash-single quote (\')
inputtagname=inputtagname.replace('\'','\\\'')
valid_tag=True
inputtagname_split=inputtagname.split(',')
for inputtag in inputtagname_split:
if not (inputtag == ''):
existingtag_split=existingtag.split(',')
for donetag in existingtag_split:
if (inputtag == donetag):
valid_tag=False
else:
inputtagname_split.remove(inputtag)
if (valid_tag):
return(','.join(inputtagname_split))
else:
print('\nCannot use the same tag as a blacktag and a whitetag.\n')
print('Use a comma \',\' to seperate multiple tag names. Try again.\n')
else:
print('\nDo not use backslash \'\\\'. Try again.\n')
#Get played ages for media types?
def get_played_age(mediaType):
defaultage=get_default_config_values('played_age_' + mediaType)
valid_age=False
while (valid_age == False):
print('Choose the number of days to wait before deleting played ' + mediaType + ' media items')
print('Valid values: 0-730500 days')
print(' -1 to disable deleting ' + mediaType + ' media items')
print('Press Enter to use default value')
age=input('Enter number of days (default ' + str(defaultage) + '): ')
if (age == ''):
valid_age=True
return(defaultage)
else:
try:
age_float=float(age)
if ((age_float % 1) == 0):
age_int=int(age_float)
if ((int(age_int) >= -1) and (int(age_int) <= 730500)):
valid_age=True
return(age_int)
else:
print('\nIgnoring Out Of Range ' + mediaType + ' Value. Try again.\n')
else:
print('\nIgnoring ' + mediaType + ' Decimal Value. Try again.\n')
except:
print('\nIgnoring Non-Whole Number ' + mediaType + ' Value. Try again.\n')
#use of hashed password removed
#hash admin password
#def get_admin_password_sha1(password):
# #password_sha1=password #input('Enter admin password (password will be hashed in config file): ')
# password_sha1=hashlib.sha1(password.encode()).hexdigest()
# return(password_sha1)
# Hash password if not hashed
#if cfg.admin_password_sha1 == '':
# cfg.admin_password_sha1=hashlib.sha1(cfg.admin_password.encode()).hexdigest()
#auth_key=''
#print('Hash:'+ cfg.admin_password_sha1)
#api call to get admin account authentication token
def get_authentication_key(server_url, username, password, server_brand):
#login info
values = {'Username' : username, 'Pw' : password}
#DATA = urllib.parse.urlencode(values)
#DATA = DATA.encode('ascii')
DATA = convert2json(values)
DATA = DATA.encode('utf-8')
xAuth = 'X-Emby-Authorization'
#assuming jellyfin will eventually change to this
#if (server_brand == 'emby'):
#xAuth = 'X-Emby-Authorization'
#else:
#xAuth = 'X-Jellyfin-Authorization'
headers = {xAuth : 'Emby UserId="' + username + '", Client="media_cleaner.py", Device="Multi-User Media Cleaner", DeviceId="MUMC", Version="' + get_script_version() + '", Token=""', 'Content-Type' : 'application/json'}
req = request.Request(url=server_url + '/Users/AuthenticateByName', data=DATA, method='POST', headers=headers)
#preConfigDebug = True
preConfigDebug = False
#api call
data=requestURL(req, preConfigDebug, 'get_authentication_key', 3)
return(data['AccessToken'])
#Unpack library data structure from config
def user_lib_builder(json_lib_entry):
lib_json=json.loads(json_lib_entry)
built_userid=[]
built_libid=[]
built_collectiontype=[]
built_networkpath=[]
built_path=[]
datapos=0
#loop thru each monitored users library entries
for currentUser in lib_json:
libid_append=''
collectiontype_append=''
networkpath_append=''
path_append=''
#loop thru each key for this user
for keySlots in currentUser:
#Store userId
if (keySlots == 'userid'):
built_userid.append(currentUser[keySlots])
if (cfg.DEBUG):
#DEBUG
print('Building library for user with Id: ' + currentUser[keySlots])
#Store library data
else:
#loop thru each library data item for this library
for keySlotLibData in currentUser[keySlots]:
#Store libId
if (keySlotLibData == 'libid'):
if (libid_append == ''):
libid_append=currentUser[keySlots][keySlotLibData]
else:
if not (currentUser[keySlots][keySlotLibData] == ''):
libid_append=libid_append + ',' + currentUser[keySlots][keySlotLibData]
else:
libid_append=libid_append + ',\'\''
if (cfg.DEBUG):
#DEBUG
print('Library Id: ' + currentUser[keySlots][keySlotLibData])
#Store collectionType
elif (keySlotLibData == 'collectiontype'):
if (collectiontype_append == ''):
collectiontype_append=currentUser[keySlots][keySlotLibData]
else:
if not (currentUser[keySlots][keySlotLibData] == ''):
collectiontype_append=collectiontype_append + ',' + currentUser[keySlots][keySlotLibData]
else:
collectiontype_append=collectiontype_append + ',\'\''
if (cfg.DEBUG):
#DEBUG
print('Collection Type: ' + currentUser[keySlots][keySlotLibData])
#Store path
elif (keySlotLibData == 'path'):
if (path_append == ''):
path_append=currentUser[keySlots][keySlotLibData]
else:
if not (currentUser[keySlots][keySlotLibData] == ''):
path_append=path_append + ',' + currentUser[keySlots][keySlotLibData]
else:
path_append=path_append + ',\'\''
if (cfg.DEBUG):
#DEBUG
print('Path: ' + currentUser[keySlots][keySlotLibData])
#Store networkPath
elif (keySlotLibData == 'networkpath'):
if (networkpath_append == ''):
networkpath_append=currentUser[keySlots][keySlotLibData]
else:
if not (currentUser[keySlots][keySlotLibData] == ''):
networkpath_append=networkpath_append + ',' + currentUser[keySlots][keySlotLibData]
else:
networkpath_append=networkpath_append + ',\'\''
if (cfg.DEBUG):
#DEBUG
print('Network Path: ' + currentUser[keySlots][keySlotLibData])
built_libid.insert(datapos,libid_append)
built_collectiontype.insert(datapos,collectiontype_append)
built_path.insert(datapos,path_append)
built_networkpath.insert(datapos,networkpath_append)
datapos+=1
return(built_userid,built_libid,built_collectiontype,built_networkpath,built_path)
#Create output string to show library information to user for them to choose
def parse_library_data_for_display(libFolder,subLibPath):
libDisplayString=' - ' + libFolder['Name']
if ('LibraryOptions' in libFolder):
if ('PathInfos' in libFolder['LibraryOptions']):
if not (len(libFolder['LibraryOptions']['PathInfos']) == 0):
if ('Path' in libFolder['LibraryOptions']['PathInfos'][subLibPath]):
libDisplayString+=' - ' + libFolder['LibraryOptions']['PathInfos'][subLibPath]['Path']
if ('NetworkPath' in libFolder['LibraryOptions']['PathInfos'][subLibPath]):
libDisplayString+=' - ' + libFolder['LibraryOptions']['PathInfos'][subLibPath]['NetworkPath']
if ('ItemId' in libFolder):
libDisplayString+=' - LibId: ' + libFolder['ItemId']
return libDisplayString
#Store the chosen library's data in temporary location for use when building blacklist and whitelist
def parse_library_data_for_temp_reference(libFolder,subLibPath,libraryTemp_dict,pos):
libraryTemp_dict[pos]['libid']=libFolder['ItemId']
if ('CollectionType' in libFolder):
libraryTemp_dict[pos]['collectiontype']=libFolder['CollectionType']
else:
libraryTemp_dict[pos]['collectiontype']='Unknown'
if ('LibraryOptions' in libFolder):
if ('PathInfos' in libFolder['LibraryOptions']):
if not (len(libFolder['LibraryOptions']['PathInfos']) == 0):
if ('Path' in libFolder['LibraryOptions']['PathInfos'][subLibPath]):
libraryTemp_dict[pos]['path']=libFolder['LibraryOptions']['PathInfos'][subLibPath]['Path']
else:
libraryTemp_dict[pos]['path']=''
if ('NetworkPath' in libFolder['LibraryOptions']['PathInfos'][subLibPath]):
libraryTemp_dict[pos]['networkpath']=libFolder['LibraryOptions']['PathInfos'][subLibPath]['NetworkPath']
else:
libraryTemp_dict[pos]['networkpath']=''
else:
libraryTemp_dict[pos]['path']=''
libraryTemp_dict[pos]['networkpath']=''
else:
libraryTemp_dict[pos]['path']=''
libraryTemp_dict[pos]['networkpath']=''
else:
libraryTemp_dict[pos]['path']=''
libraryTemp_dict[pos]['networkpath']=''
return libraryTemp_dict
#Parse library Paths
def cleanup_library_paths(libPath_str):
libPath_str=libPath_str.replace('\\','/')
return(libPath_str)
#API call to get library folders; choose which libraries to blacklist and whitelist
def get_library_folders(server_url, auth_key, infotext, user_policy, user_id, user_name, mandatory, library_matching_behavior):
#get all library paths for a given user
#Request for libraries (i.e. movies, tvshows, audio, etc...)
req_folders=(server_url + '/Library/VirtualFolders?api_key=' + auth_key)
#Request for channels (i.e. livetv, trailers, etc...)
#req_channels=(server_url + '/Channels?api_key=' + auth_key)
#preConfigDebug = True
preConfigDebug = False
#api calls
data_folders = requestURL(req_folders, preConfigDebug, 'get_media_folders', 3)
#data_channels = requestURL(req_channels, preConfigDebug, 'get_media_channels', 3)['Items']
#define empty dictionaries
libraryTemp_dict=defaultdict(dict)
library_dict=defaultdict(dict)
not_library_dict=defaultdict(dict)
#define empty sets
#libraryPath_set=set()
library_tracker=[]
enabledFolderIds_set=set()
#delete_channels=[]
#remove all channels that are not 'Trailers'
#for channel in data_channels:
#if not (channel['SortName'] == 'Trailers'):
#delete_channels.append(channel)
#reverse so we delete from the bottom up
#delete_channels.reverse()
#for delchan in range(len(delete_channels)):
#data_channels.pop(delchan)
#Check if this user has permission to access to all libraries or only specific libraries
if not (user_policy['EnableAllFolders']):
for okFolders in range(len(user_policy['EnabledFolders'])):
enabledFolderIds_set.add(user_policy['EnabledFolders'][okFolders])
#Check if this user has permission to access to all channels or only specific channels
#if not (user_policy['EnableAllChannels']):
#for okChannels in range(len(user_policy['EnabledChannels'])):
#enabledFolderIds_set.add(user_policy['EnabledChannels'][okChannels])
i=0
#Populate all libraries into a "not chosen" data structure
# i.e. if blacklist chosen all libraries start out as whitelisted
# i.e. if whitelist chosen all libraries start out as blacklisted
for libFolder in data_folders:
if (('ItemId' in libFolder) and ('CollectionType' in libFolder) and ((enabledFolderIds_set == set()) or (libFolder['ItemId'] in enabledFolderIds_set))):
for subLibPath in range(len(libFolder['LibraryOptions']['PathInfos'])):
if not ('userid' in not_library_dict):
not_library_dict['userid']=user_id
not_library_dict[i]['libid']=libFolder['ItemId']
not_library_dict[i]['collectiontype']=libFolder['CollectionType']
if (('Path' in libFolder['LibraryOptions']['PathInfos'][subLibPath])):
not_library_dict[i]['path']=libFolder['LibraryOptions']['PathInfos'][subLibPath]['Path']
else:
not_library_dict[i]['path']=''
if (('NetworkPath' in libFolder['LibraryOptions']['PathInfos'][subLibPath])):
not_library_dict[i]['networkpath']=libFolder['LibraryOptions']['PathInfos'][subLibPath]['NetworkPath']
else:
not_library_dict[i]['networkpath']=''
i += 1
#Populate all channels into a "not chosen" data structure
# i.e. if blacklist chosen all channels start out as whitelisted
# i.e. if whitelist chosen all channels start out as blacklisted
#for libChannel in data_channels:
#if (('Id' in libChannel) and ('Type' in libChannel) and ((enabledFolderIds_set == set()) or (libChannel['Id'] in enabledFolderIds_set))):
#if not ('userid' in not_library_dict):
#not_library_dict['userid']=user_id
#not_library_dict[i]['libid']=libChannel['Id']
#not_library_dict[i]['collectiontype']=libChannel['Type']
#not_library_dict[i]['path']=''
#not_library_dict[i]['networkpath']=''
#i += 1
#Go thru libaries this user has permissions to access and show them on the screen
stop_loop=False
first_run=True
libInfoPrinted=False
while (stop_loop == False):
j=0
k=0
showpos_correlation={}
for libFolder in data_folders:
if (('ItemId' in libFolder) and ('CollectionType' in libFolder) and ((enabledFolderIds_set == set()) or (libFolder['ItemId'] in enabledFolderIds_set))):
for subLibPath in range(len(libFolder['LibraryOptions']['PathInfos'])):
if ((library_matching_behavior == 'byId') and ('ItemId' in libFolder)):
#option made here to check for either ItemId or Path when deciding what to show
# when ItemId libraries with multiple folders but same libId will be removed together
# when Path libraries with multiple folders but the same libId will be removed individually
if ((library_matching_behavior == 'byId') and ( not (libFolder['ItemId'] in library_tracker))):
print(str(j) + parse_library_data_for_display(libFolder,subLibPath))
libInfoPrinted=True
else:
#show blank entry
print(str(j) + ' - ' + libFolder['Name'] + ' - ' )
libInfoPrinted=True
if not ('userid' in libraryTemp_dict):
libraryTemp_dict['userid']=user_id
libraryTemp_dict=parse_library_data_for_temp_reference(libFolder,subLibPath,libraryTemp_dict,k)
elif ((library_matching_behavior == 'byPath') and ('Path' in libFolder['LibraryOptions']['PathInfos'][subLibPath])):
#option made here to check for either ItemId or Path when deciding what to show
# when ItemId libraries with multiple folders but same libId will be removed together
# when Path libraries with multiple folders but the same libId will be removed individually
if ((library_matching_behavior == 'byPath') and ( not (libFolder['LibraryOptions']['PathInfos'][subLibPath]['Path'] in library_tracker))):
print(str(j) + parse_library_data_for_display(libFolder,subLibPath))
libInfoPrinted=True
else:
#show blank entry
print(str(j) + ' - ' + libFolder['Name'] + ' - ' )
libInfoPrinted=True
if not ('userid' in libraryTemp_dict):
libraryTemp_dict['userid']=user_id
libraryTemp_dict=parse_library_data_for_temp_reference(libFolder,subLibPath,libraryTemp_dict,k)
elif ((library_matching_behavior == 'byNetworkPath') and ('NetworkPath' in libFolder['LibraryOptions']['PathInfos'][subLibPath])):
#option made here to check for either ItemId or Path when deciding what to show
# when ItemId libraries with multiple folders but same libId will be removed together
# when Path libraries with multiple folders but the same libId will be removed individually
if ((library_matching_behavior == 'byNetworkPath') and ( not (libFolder['LibraryOptions']['PathInfos'][subLibPath]['NetworkPath'] in library_tracker))):
print(str(j) + parse_library_data_for_display(libFolder,subLibPath))
libInfoPrinted=True
else:
#show blank entry
print(str(j) + ' - ' + libFolder['Name'] + ' - ' )
libInfoPrinted=True
if not ('userid' in libraryTemp_dict):
libraryTemp_dict['userid']=user_id
libraryTemp_dict=parse_library_data_for_temp_reference(libFolder,subLibPath,libraryTemp_dict,k)
if (libInfoPrinted):
showpos_correlation[k]=j
k += 1
if (((library_matching_behavior == 'byPath') or (library_matching_behavior == 'byNetworkPath')) and (libInfoPrinted)):
libInfoPrinted=False
j += 1
if ((library_matching_behavior == 'byId') and (libInfoPrinted)):
libInfoPrinted=False
j += 1
#Go thru channels this user has permissions to access and show them on the screen
#for libChannel in data_channels:
#if (('Id' in libChannel) and ('Type' in libChannel) and ((enabledFolderIds_set == set()) or (libChannel['Id'] in enabledFolderIds_set))):
#if not (libChannel['Id'] in library_tracker):
#print(str(j) + ' - ' + libChannel['Name'] + ' - ' + libChannel['Type'] + ' - LibId: ' + libChannel['Id'])
#else:
#show blank entry
#print(str(j) + ' - ' + libChannel['Name'] + ' - ' )
#if not ('userid' in libraryTemp_dict):
#libraryTemp_dict['userid']=user_id
#libraryTemp_dict[j]['libid']=libChannel['Id']
#libraryTemp_dict[j]['collectiontype']=libChannel['Type']
#libraryTemp_dict[j]['path']=''
#libraryTemp_dict[j]['networkpath']=''
#j += 1
print('')
#Wait for user input telling us which library they are have selected
if (j >= 1):
print(infotext)
#Get user input for libraries
if ((mandatory) and (first_run)):
first_run=False
path_number=input('Select one or more libraries.\n*Use a comma to separate multiple selections.\nMust select at least one library to monitor: ')
elif (mandatory):
path_number=input('Select one or more libraries.\n*Use a comma to separate multiple selections.\nLeave blank when finished: ')
else:
path_number=input('Select one or more libraries.\n*Use a comma to separate multiple selections.\nLeave blank for none or when finished: ')
#Cleanup input to allow multiple library choices at the same time
if not (path_number == ''):
#replace spaces with commas (assuming people will use spaces because the space bar is bigger and easier to push)
comma_path_number=path_number.replace(' ',',')
#convert string to list
list_path_number=comma_path_number.split(',')
#remove blanks
while ('' in list_path_number):
list_path_number.remove('')
else: #(path_number == ''):
#convert string to list
list_path_number=path_number.split(',')
#loop thru user chosen libraries
for input_path_number in list_path_number:
try:
if ((input_path_number == '') and (len(library_tracker) == 0) and (mandatory)):
print('\nMust select at least one library to monitor for this user. Try again.\n')
elif (input_path_number == ''):
#Add valid library selecitons to the library dicitonary
if not ('userid' in library_dict):
library_dict['userid']=libraryTemp_dict['userid']
if not ('username' in library_dict):
library_dict['username']=user_name
if not ('username' in not_library_dict):
not_library_dict['username']=user_name
stop_loop=True
print('')
else:
path_number_float=float(input_path_number)
if ((path_number_float % 1) == 0):
path_number_int=int(path_number_float)
if ((path_number_int >= 0) and (path_number_int < j)):
#Add valid library selecitons to the library dicitonary
if not ('userid' in library_dict):
library_dict['userid']=libraryTemp_dict['userid']
if not ('username' in library_dict):
library_dict['username']=user_name
for showpos in showpos_correlation:
if (showpos_correlation[showpos] == path_number_int):
for checkallpos in libraryTemp_dict:
libMatchFound=False
if not ((checkallpos == 'userid') or (checkallpos in library_dict)):
if ((library_matching_behavior == 'byId') and (libraryTemp_dict[showpos]['libid'] == libraryTemp_dict[checkallpos]['libid'])):
libMatchFound=True
library_dict[checkallpos]['libid']=libraryTemp_dict[checkallpos]['libid']
library_dict[checkallpos]['collectiontype']=libraryTemp_dict[checkallpos]['collectiontype']
library_dict[checkallpos]['path']=libraryTemp_dict[checkallpos]['path']
library_dict[checkallpos]['networkpath']=libraryTemp_dict[checkallpos]['networkpath']
elif ((library_matching_behavior == 'byPath') and (libraryTemp_dict[showpos]['path'] == libraryTemp_dict[checkallpos]['path'])):
libMatchFound=True
library_dict[checkallpos]['libid']=libraryTemp_dict[checkallpos]['libid']
library_dict[checkallpos]['collectiontype']=libraryTemp_dict[checkallpos]['collectiontype']
library_dict[checkallpos]['path']=libraryTemp_dict[checkallpos]['path']
library_dict[checkallpos]['networkpath']=libraryTemp_dict[checkallpos]['networkpath']
elif ((library_matching_behavior == 'byNetworkPath') and (libraryTemp_dict[showpos]['networkpath'] == libraryTemp_dict[checkallpos]['networkpath'])):
libMatchFound=True
library_dict[checkallpos]['libid']=libraryTemp_dict[checkallpos]['libid']
library_dict[checkallpos]['collectiontype']=libraryTemp_dict[checkallpos]['collectiontype']
library_dict[checkallpos]['path']=libraryTemp_dict[checkallpos]['path']
library_dict[checkallpos]['networkpath']=libraryTemp_dict[checkallpos]['networkpath']
if (libMatchFound):
#The chosen library is removed from the "not chosen" data structure
#Remove valid library selecitons from the not_library dicitonary
if (checkallpos in not_library_dict):
if not ('username' in not_library_dict):
not_library_dict['username']=user_name
not_library_dict.pop(checkallpos)
#The chosen library is added to the "chosen" data structure
# Add library ID/Path to chosen list type behavior
if (( not (library_dict[checkallpos].get('libid') == '')) and (library_matching_behavior == 'byId')):
library_tracker.append(library_dict[checkallpos]['libid'])
elif (( not (library_dict[checkallpos].get('path') == '')) and (library_matching_behavior == 'byPath')):
library_tracker.append(library_dict[checkallpos]['path'])
elif (( not (library_dict[checkallpos].get('networkpath') == '')) and (library_matching_behavior == 'byNetworkPath')):
library_tracker.append(library_dict[checkallpos]['networkpath'])
#When all libraries selected we can automatically exit the library chooser
if (len(library_tracker) >= len(showpos_correlation)):
stop_loop=True
print('')
else:
stop_loop=False
#DEBUG
if(preConfigDebug):
print('libraryTemp_dict = ' + str(libraryTemp_dict) + '\n')
print('library_dict = ' + str(library_dict) + '\n')
print('not_library_dict = ' + str(not_library_dict) + '\n')
print('library_tracker = ' + str(library_tracker) + '\n')
else:
print('\nIgnoring Out Of Range Value: ' + input_path_number + '\n')
else:
print('\nIgnoring Decimal Value: ' + input_path_number + '\n')
except:
print('\nIgnoring Non-Whole Number Value: ' + input_path_number + '\n')
#Determine how many entries are in the "choosen" and "not choosen" data structures
#When both have >1 parse both data structures
if ((len(library_dict) > 2) and (len(not_library_dict) > 2)):
for entry in library_dict:
if not ((entry == 'userid') or (entry == 'username')):
library_dict[entry]['path']=cleanup_library_paths(library_dict[entry].get('path'))
library_dict[entry]['networkpath']=cleanup_library_paths(library_dict[entry].get('networkpath'))
for entry in not_library_dict:
if not ((entry == 'userid') or (entry == 'username')):
not_library_dict[entry]['path']=cleanup_library_paths(not_library_dict[entry].get('path'))
not_library_dict[entry]['networkpath']=cleanup_library_paths(not_library_dict[entry].get('networkpath'))
#libraries for blacklist and whitelist
return(not_library_dict,library_dict)
#When only one is >1 parse that data structur
elif ((len(library_dict) == 2) and (len(not_library_dict) > 2)):
for entry in not_library_dict:
if not ((entry == 'userid') or (entry == 'username')):
not_library_dict[entry]['path']=cleanup_library_paths(not_library_dict[entry].get('path'))
not_library_dict[entry]['networkpath']=cleanup_library_paths(not_library_dict[entry].get('networkpath'))
#libraries for blacklist and whitelist
return(not_library_dict,library_dict)
#When only one is >1 parse that data structur
elif ((len(library_dict) > 2) and (len(not_library_dict) == 2)):
for entry in library_dict:
if not ((entry == 'userid') or (entry == 'username')):
library_dict[entry]['path']=cleanup_library_paths(library_dict[entry].get('path'))
library_dict[entry]['networkpath']=cleanup_library_paths(library_dict[entry].get('networkpath'))
#libraries for blacklist and whitelist
return(not_library_dict,library_dict)
#If both are somehow zero just return the data without parsing
else: #((len(library_dict) == 0) and (len(not_library_dict) == 0)):
#This should never happen
#empty libraries for blacklist and whitelist
return(not_library_dict,library_dict)
#API call to get all user accounts
#Choose account(s) this script will use to delete played media
#Choosen account(s) do NOT need to have "Allow Media Deletion From" enabled in the UI
def get_users_and_libraries(server_url,auth_key,library_setup_behavior,updateConfig,library_matching_behavior):
#Get all users
req=(server_url + '/Users?api_key=' + auth_key)
#preConfigDebug = True
preConfigDebug = False
#api call
data=requestURL(req, preConfigDebug, 'get_users', 3)
#define empty userId dictionary
userId_dict={}
#define empty monitored library dictionary
userId_bllib_dict={}
#define empty whitelisted library dictionary
userId_wllib_dict={}
#define empty userId set
userId_set=set()
userId_ReRun_set=set()
user_keys_json=''
#user_bl_libs_json=''
#user_wl_libs_json=''
#Check if not running for the first time
if (updateConfig):
#Build the library data from the data structures stored in the configuration file
bluser_keys_json_verify,user_bllib_keys_json,user_bllib_collectiontype_json,user_bllib_netpath_json,user_bllib_path_json=user_lib_builder(cfg.user_bl_libs)
#Build the library data from the data structures stored in the configuration file
wluser_keys_json_verify,user_wllib_keys_json,user_wllib_collectiontype_json,user_wllib_netpath_json,user_wllib_path_json=user_lib_builder(cfg.user_wl_libs)
#verify userId are in same order for both blacklist and whitelist libraries
if (bluser_keys_json_verify == wluser_keys_json_verify):
user_keys_json = bluser_keys_json_verify
else:
raise RuntimeError('\nUSERID_ERROR: cfg.user_bl_libs or cfg.user_wl_libs has been modified in media_cleaner_config.py; userIds need to be in the same order for both')
i=0
usernames_userkeys=json.loads(cfg.user_keys)
#Pre-populate the existing userkeys and libraries; only new users are fully shown; existing user will display minimal info
for rerun_userkey in user_keys_json:
userId_set.add(rerun_userkey)
if (rerun_userkey == bluser_keys_json_verify[i]):
userId_bllib_dict[rerun_userkey]=json.loads(cfg.user_bl_libs)[i]
for usernames_userkeys_str in usernames_userkeys:
usernames_userkeys_list=usernames_userkeys_str.split(":")
if (len(usernames_userkeys_list) == 2):
if (usernames_userkeys_list[1] == rerun_userkey):
userId_bllib_dict[rerun_userkey]['username']=usernames_userkeys_list[0]
else:
raise ValueError('\nUnable to edit config when username contains colon (:).')
else:
raise ValueError('\nValueError: Order of user_keys and user_wl libs are not in the same order.')
if (rerun_userkey == wluser_keys_json_verify[i]):
userId_wllib_dict[rerun_userkey]=json.loads(cfg.user_wl_libs)[i]
for usernames_userkeys_str in usernames_userkeys:
usernames_userkeys_list=usernames_userkeys_str.split(":")
if (len(usernames_userkeys_list) == 2):
if (usernames_userkeys_list[1] == rerun_userkey):
userId_wllib_dict[rerun_userkey]['username']=usernames_userkeys_list[0]
else:
raise ValueError('\nUnable to edit config when username contains colon (:).')
else:
raise ValueError('\nValueError: Order of user_keys and user_bl libs are not in the same order.')
i += 1
#Uncomment if the config editor should only allow adding new users
#This means the script will not allow editing exisitng users until a new user is added
#if ((len(user_keys_json)) == (len(data))):
#print('-----------------------------------------------------------')
#print('No new user(s) found.')
#print('-----------------------------------------------------------')
#print('Verify new user(s) added to the Emby/Jellyfin server.')
#return(userId_bllib_dict, userId_wllib_dict)
stop_loop=False
single_user=False
one_user_selected = False
#Loop until all user accounts selected or until manually stopped with a blank entry
while (stop_loop == False):
i=0
#Determine if we are looking at a mulitple user setup or a single user setup
if (len(data) > 1):
for user in data:
if not (user['Id'] in userId_set):
print(str(i) +' - '+ user['Name'] + ' - ' + user['Id'])
userId_dict[i]=user['Id']
else:
#show blank entry
print(str(i) +' - '+ user['Name'] + ' - ')
userId_dict[i]=user['Id']
i += 1
else: #Single user setup
single_user=True
for user in data:
userId_dict[i]=user['Id']
print('')
#When single user we can prepare to exit after their libraries are selected
if ((i == 0) and (single_user == True)):
user_number='0'
#When multiple explain how to select each user
elif ((i >= 1) and (one_user_selected == False)):
user_number=input('Select one user at a time.\nEnter number of the user to monitor: ')
print('')
#When multiple explain how to select each user; when coming back to the user selection show this
else: #((i >= 1) and (one_user_selected == True)):
print('Monitoring multiple users is possible.')
print('When multiple users are selected; the user with the oldest last played time will determine if media can be deleted.')
user_number=input('Select one user at a time.\nEnter number of the next user to monitor; leave blank when finished: ')
print('')
try:
#When single user we know the loop will stop right after libraries are chosen
if ((user_number == '0') and (single_user == True)):
stop_loop=True
one_user_selected=True
user_number_int=int(user_number)
userId_set.add(userId_dict[user_number_int])
#Depending on library setup behavior the chosen libraries will either be treated as blacklisted libraries or whitelisted libraries
if (library_setup_behavior == 'blacklist'):
message='Enter number of the library folder to blacklist (aka monitor) for the selected user.\nMedia in blacklisted library folder(s) will be monitored for deletion.'
userId_wllib_dict[userId_dict[user_number_int]],userId_bllib_dict[userId_dict[user_number_int]]=get_library_folders(server_url,auth_key,message,data[user_number_int]['Policy'],data[user_number_int]['Id'],data[user_number_int]['Name'],False,library_matching_behavior)
else: #(library_setup_behavior == 'whitelist'):
message='Enter number of the library folder to whitelist (aka ignore) for the selcted user.\nMedia in whitelisted library folder(s) will be excluded from deletion.'
userId_bllib_dict[userId_dict[user_number_int]],userId_wllib_dict[userId_dict[user_number_int]]=get_library_folders(server_url,auth_key,message,data[user_number_int]['Policy'],data[user_number_int]['Id'],data[user_number_int]['Name'],False,library_matching_behavior)
#We get here when we are done selecting users to monitor
elif ((user_number == '') and (not (len(userId_set) == 0))):
stop_loop=True
print('')
#We get here if there are muliple users and we tried not to select any; at least one must be selected
elif ((user_number == '') and (len(userId_set) == 0)):
print('\nMust select at least one user. Try again.\n')
#When multiple users we get here to allow selecting libraries for the specified user
elif not (user_number == ''):
user_number_float=float(user_number)
if ((user_number_float % 1) == 0):
user_number_int=int(user_number_float)
if ((user_number_int >= 0) and (user_number_int < i)):
one_user_selected=True
userId_set.add(userId_dict[user_number_int])
if (updateConfig):
userId_ReRun_set.add(userId_dict[user_number_int])