· 5 years ago · Oct 18, 2020, 07:54 PM
1#!/bin/env python
2#
3# Copyright 2020 Wisperer/WSilence
4# Copyright 2020 EraYaN
5#
6# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7#
8# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9#
10# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11#
12
13import os
14import json
15import glob
16import requests
17import dicttoxml
18import xml.dom.minidom
19import shutil
20import urllib.parse
21from xml.dom.minidom import parseString
22from pathlib import Path
23from datetime import datetime
24# If dicttoxlm isn't installed by default just run 'pip install dictoxml' #
25
26# Define Global Varables #
27address = 'http://localhost:8111'
28overwrite = True # Overwrite output files.
29set_permissions = False # Only for posix
30nfo_permissions = 0o660
31image_permissions = 0o640
32languages = ['EN', 'X-JAT', 'JA']
33api_version = '3.0'
34sep = os.path.sep
35
36shows = []
37
38art_file_types = {
39 "image/jpeg": '.jpg',
40 "image/png": '.png'
41}
42
43extlist = ('*.mkv', '*.avi', '*.mp4', '*.ogm')
44
45# Grab Shoko Auth Key #
46
47def authentication():
48
49 ApiHeaders = {
50 'Content-Type': 'application/json',
51 'Accept': 'application/json',
52 }
53
54 ApiData = '{"user": "Default", "pass": "", "device": "CLI"}'
55
56 auth = requests.post(address + '/api/auth',
57 headers=ApiHeaders, data=ApiData)
58 return json.loads(auth.text)['apikey']
59
60# Get Import Folder #
61
62
63def grabfolder():
64 FolderHeader = {
65 'accept': 'application/json',
66 'apikey': key
67 }
68
69 topfolder = requests.get(
70 '{0}/api/v{1}/ImportFolder'.format(address, api_version), headers=FolderHeader)
71 returnfolder = json.loads(topfolder.text)
72 print(returnfolder)
73 return [element['Path']
74 for element in returnfolder if element.get('DropFolderType',0) != 1]
75
76
77# Grabbing And Exporting Episode Data #
78def episodeinfo(fullpath, epname, epath, noext):
79 Headers = {
80 'accept': 'application/json',
81 'apikey': key
82 }
83
84 fileinfo = requests.get(
85 '{0}/api/v{1}/File/PathEndsWith/{2}'.format(address, api_version, urllib.parse.quote(fullpath)), headers=Headers)
86
87 if fileinfo.status_code != 200:
88 print("File {} is not an episode.".format(epath + sep + noext))
89 return
90
91 # Mapping Data from Shoko to Jellyfin NFO #
92
93 data = json.loads(fileinfo.text)
94
95 if len(data) < 1:
96 print("File {} is not an episode.".format(epath + sep + noext))
97 return
98
99 if len(data) > 1:
100 print("File {} matched multiple episodes, taking first result.".format(epath + sep + noext))
101
102 data=data[0]
103
104 file_id = data.get('ID', None)
105
106 episode_id = data.get('SeriesIDs')[0].get('EpisodeIDs')[0]
107 series_id = data.get('SeriesIDs')[0].get('SeriesID')
108
109 if 'ID' in episode_id:
110 basic_info = {}
111 basic_info_res = requests.get(
112 '{0}/api/v{1}/Episode/{2}'.format(address, api_version, episode_id['ID']), headers=Headers)
113
114 if basic_info_res.status_code == 200:
115 basic_info = json.loads(basic_info_res.text)
116
117 basic_info_anidb = {}
118 if 'AniDB' in episode_id:
119 basic_info_anidb_res = requests.get(
120 '{0}/api/v{1}/Episode/{2}/AniDB'.format(address, api_version, episode_id['ID']), headers=Headers)
121
122 if basic_info_anidb_res.status_code == 200:
123 basic_info_anidb = json.loads(basic_info_anidb_res.text)
124
125 basic_info_tvdb = {}
126 if 'TvDB' in episode_id:
127 basic_info_tvdb_res = requests.get(
128 '{0}/api/v{1}/Episode/{2}/TvDB'.format(address, api_version, episode_id['ID']), headers=Headers)
129
130 if basic_info_tvdb_res.status_code == 200:
131 basic_info_tvdb_tmp = json.loads(basic_info_tvdb_res.text)
132 if len(basic_info_tvdb_tmp) > 0:
133 basic_info_tvdb=basic_info_tvdb_tmp[0]
134
135
136 eplot = basic_info_anidb.get('Description', None)
137 if eplot is None:
138 eplot = basic_info_tvdb.get('Description', None)
139 etitle = None
140 for lang in languages:
141 filtered = list(filter(lambda title: title['Language']==lang and title['Language']!='short', basic_info_anidb.get('Titles', [])))
142 if len(filtered) > 0:
143 etitle = filtered[0]['Name']
144 break
145 if etitle is None:
146 etitle = basic_info_tvdb.get('Title', None)
147
148 eair = basic_info_anidb.get('AirDate', basic_info_tvdb.get('AirDate', None))
149 eyear = None
150 if eair:
151 dt = datetime.strptime(eair,"%Y-%m-%d")
152 eyear = dt.year
153
154 if basic_info_tvdb:
155 episode = basic_info_tvdb.get('Number', None)
156 season = basic_info_tvdb.get('Season', 0)
157 elif basic_info_anidb:
158 episode = basic_info_tvdb.get('EpisodeNumber', None)
159 season = 0
160
161 thumb = basic_info_tvdb.get('Thumbnail', None)
162
163 # Create Dictionary From Mapped Data #
164
165 episode = {
166 "plot": eplot,
167 "title": etitle,
168 "year": eyear,
169 "aired": eair,
170 "episode": episode,
171 "season": season,
172 }
173
174 # print(show)
175
176 # Clean up emtpy items
177 episode = {k:v for k,v in episode.items() if v is not None}
178
179 # Create and Write XML NFO File #
180
181 episodexml = dicttoxml.dicttoxml(
182 episode, custom_root='episodedetails', attr_type=False)
183 episodeparse = xml.dom.minidom.parseString(episodexml)
184 episodeprint = episodeparse.toprettyxml()
185 if os.path.isfile(epath + sep + noext + ".nfo") and not overwrite:
186 print("Nfo File For This Episode Exists")
187 else:
188 with open(epath + sep + noext + ".nfo", "w", encoding='utf8') as episodefile:
189 episodefile.write(episodeprint)
190 print("Saved NFO {}".format(epath + sep + noext + ".nfo"))
191 if os.name == "posix" and set_permissions:
192 os.chmod(epath + sep + noext + ".nfo", nfo_permissions)
193
194 if thumb:
195 write_single_image(thumb, epath + sep + noext + "-thumb")
196
197 #write_image({'thumb':[thumb]}, 'thumb', epath + sep + noext + "-thumb")
198 #write_image(art, 'fanart', epath + sep + noext + "-fanart")
199
200 return (series_id, episode_id)
201
202# More Data Grabbing #
203def tvshowinfo(series_id, epath):
204
205 Headers = {
206 'accept': 'application/json',
207 'apikey': key
208 }
209
210 if 'ID' in series_id:
211 basic_info = {}
212 basic_info_res = requests.get(
213 '{0}/api/v{1}/Series/{2}'.format(address, api_version, series_id['ID']), headers=Headers)
214
215 if basic_info_res.status_code == 200:
216 basic_info = json.loads(basic_info_res.text)
217
218 basic_info_anidb = {}
219 if 'AniDB' in series_id:
220 basic_info_anidb_res = requests.get(
221 '{0}/api/v{1}/Series/{2}/AniDB'.format(address, api_version, series_id['ID']), headers=Headers)
222
223 if basic_info_anidb_res.status_code == 200:
224 basic_info_anidb = json.loads(basic_info_anidb_res.text)
225
226 basic_info_tvdb = {}
227 if 'TvDB' in series_id:
228 basic_info_tvdb_res = requests.get(
229 '{0}/api/v{1}/Series/{2}/TvDB'.format(address, api_version, series_id['ID']), headers=Headers)
230
231 if basic_info_tvdb_res.status_code == 200:
232 basic_info_tvdb_tmp = json.loads(basic_info_tvdb_res.text)
233 if len(basic_info_tvdb_tmp) > 0:
234 basic_info_tvdb=basic_info_tvdb_tmp[0]
235
236 splot = basic_info_anidb.get('Description', None)
237 if splot is None:
238 splot = basic_info_tvdb.get('Description', None)
239 stitle = None
240 for lang in languages:
241 filtered = list(filter(lambda title: title['Language']==lang and title['Language']!='short', basic_info_anidb.get('Titles', [])))
242 if len(filtered) > 0:
243 stitle = filtered[0]['Name']
244 break
245 if stitle is None:
246 stitle = basic_info_tvdb.get('Title', None)
247
248 sair = basic_info_anidb.get('AirDate', basic_info_tvdb.get('AirDate', None))
249 syear = None
250 if sair:
251 dt = datetime.strptime(sair,"%Y-%m-%d")
252 syear = dt.year
253
254 images = basic_info_tvdb.get('images', {})
255 posters = images.get('Posters', [])
256 fanarts = images.get('Fanarts', [])
257 banners = images.get('Banners', [])
258
259 # Create Dictionary From Mapped Data #
260
261 # print(tvdbid)
262 show = {
263 "plot": splot,
264 "title": stitle,
265 "year": syear,
266 "premiered": sair,
267 "tvdbid": (series_id.get('TvDB') or [None])[0],
268 "anidbid": series_id.get('AniDB',None),
269 }
270
271 # Clean up emtpy items
272 show = {k:v for k,v in show.items() if v is not None}
273
274 # Creating tvshow.nfo XML Table and Writing It #
275
276 showxml = dicttoxml.dicttoxml(
277 show, custom_root='tvshowdetails', attr_type=False)
278 showparse = xml.dom.minidom.parseString(showxml)
279 showprint = showparse.toprettyxml()
280 if os.path.isfile(epath + sep + "tvshow.nfo") and not overwrite:
281 print("TV show NFO exists")
282 else:
283 with open(epath + sep + "tvshow.nfo", "w", encoding='utf8') as showfile:
284 showfile.write(showprint)
285 print("Saved NFO {}".format(epath + sep + "tvshow.nfo"))
286 if os.name == "posix" and set_permissions:
287 os.chmod(epath + sep + "tvshow.nfo", nfo_permissions)
288
289 if len(posters) > 0:
290 write_single_image(posters[0], epath + sep + "poster")
291 if len(fanarts) > 0:
292 write_single_image(fanarts[0], epath + sep + "fanart")
293 if len(banners) > 0:
294 write_single_image(banners[0], epath + sep + "banner")
295
296
297def write_image(art, kind, path_noext):
298 if kind in art:
299 if len(art[kind]) > 1:
300 print("Multiple {} images for {}, using first one.".format(
301 kind, path_noext))
302 # thumb = poster
303 if len(art[kind]) > 0:
304 write_single_image(art[kind][0], path_noext)
305
306def write_single_image(image, path_noext):
307 exists = []
308 for ext in art_file_types.values():
309 if os.path.isfile(path_noext+ext):
310 exists.append(path_noext+ext)
311
312 if len(exists) == 0 or overwrite:
313 for old_file in exists:
314 os.remove(old_file)
315 print("Removed {0}".format(old_file))
316
317 thumb = requests.get(
318 "{0}/api/v{1}/Image/{2}/{3}/{4}".format(address, api_version, image['Source'], image['Type'], image['ID']), stream=True)
319 content_type = thumb.headers.get("Content-Type", None)
320 if content_type not in art_file_types:
321 print(
322 "ERROR: unknown content-type for artwork {0}".format(content_type))
323 else:
324 with open(path_noext + art_file_types[content_type], "wb") as thumbfile:
325 thumb.raw.decode_content = True
326 shutil.copyfileobj(thumb.raw, thumbfile)
327 print("Saved {0}".format(
328 path_noext + art_file_types[content_type]))
329 if os.name == "posix" and set_permissions:
330 os.chmod(path_noext + art_file_types[content_type], image_permissions)
331
332
333if __name__ == "__main__":
334 print("Starting...")
335 key = authentication()
336
337 print(key)
338 importfolders = grabfolder()
339 print(importfolders)
340 for ext in extlist:
341 for importfolder in importfolders:
342 for files in glob.iglob(importfolder + "**" + sep + ext, recursive=True):
343 epname = os.path.basename(files)
344 epath = os.path.dirname(files)
345 noext = os.path.splitext(epname)[0]
346 print(epname)
347 # episodeinfo(epname, epath, noext)
348 result = episodeinfo(files, epname, epath, noext)
349 if result is not None:
350 series_id , episode_id = result
351 if "ID" not in series_id:
352 print("Some Kind of Error Occured")
353 else:
354 tvshowinfo(series_id, epath)
355