├── wrapper.sh ├── config.ini_example ├── README.md ├── ruv_downloader.py └── krakkaruv_skitafix.py /wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | home_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 4 | 5 | cd $home_dir 6 | source ./venv/bin/activate 7 | python3 ./ruv_downloader.py $@ 8 | -------------------------------------------------------------------------------- /config.ini_example: -------------------------------------------------------------------------------- 1 | 2 | 3 | [config] 4 | ffmpeg_path = /path/to/ffmpeg_dir/ 5 | colors = True 6 | plexify = True ## Virkjar þá á öll libraries 7 | plexify_clashes = True ## Virkjar þá á öll libraries 8 | debug = False ## Default, ekki prenta debug info 9 | disable_m3u8_output = False ## Default, ef sett sem True er m3u8 downloader info route-að í /dev/null 10 | 11 | 12 | 13 | 14 | 15 | 16 | [autodownload] 17 | thattur_1 = True 18 | thattur_2 = False 19 | 20 | 21 | [thattur_1] 22 | show_id = 123456 23 | dl_dir = /path/to/dir/for/thattur_1 24 | #Disable-ar plexify fyrir thatt_1 25 | plexify = False 26 | plexify_clashes = False 27 | 28 | 29 | [thattur_2] 30 | show_id = 98765 31 | dl_dir = /path/to/dir/for/thattur_2 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Virkar ekki lengur 2 | # [Ný Útgáfa hér](https://github.com/gbit-is/ruv_downloader_v2) 3 | 4 | # 5 | # 6 | # 7 | 8 | 9 | # ruv_downloader 10 | 11 | # About 12 | Hugbúnaður til að sjálfvirkt búa til öryggisafrit af þáttum á rúv og save-ar sem mp4 13 | Ath: Ennþá mjög hrátt, ekkert error handling t.d 14 | 15 | # Install 16 | 17 | Á linux/wsl: 18 | 19 | - Install ffmpeg however you want to 20 | - git clone git@github.com:gbit-is/ruv_downloader.git 21 | - cd ruv_downloader 22 | - python3 -m venv ./venv 23 | - source venv/bin/activate 24 | - pip install requests 25 | - pip install m3u8_To_MP4 26 | 27 | Á windows: 28 | ..... nota wsl bara ? 29 | 30 | Á mac: 31 | - eins og á linux, nema settu upp ffmpeg hvernig sem þið makkafólk gerið það og setjið ffmpeg pathinn í config[config][ffmpeg_path] 32 | 33 | # How to use 34 | 35 | ./ruv_downloader.py list 36 | Skilar lista af ID's á þáttum og nafni þáttana 37 | 38 | ./ruv_downloader.py auto 39 | les config.ini skjalið, lúppar gegnum configured þætti, athugar hvort það sé búið að downloada þeim og downloadar þeim ef ekki 40 | 41 | -- config.ini skjalið 42 | 43 | Ef þú ert t.d á mac og ert ekki með ffmpeg í path, þá geturðu búið til [config][ffmpeg_path] til þess að taka fram ffmpeg pathinn og honum er bætt við í os.path 44 | 45 | downloaderinn les [autodownload] kaflann, þar býrðu til "objects" og merkir þau sem True ef þú vilt downloada þeim (getur sett í False eða hvaða annan streng sem er til þess að disable-a það download án þess að eyða skilgreiningunni 46 | 47 | fyrir hvert object í [autodownload] kaflanum gerirðu nýjan kafla, sem vísar í show_id (tekið úr ./ruv_downloader.py list) og hvar á að save-a skjölin. 48 | Einnig er hægt að taka fram "plexify", sem að downloadar þá "poster" mynd af þættinum í show.jpg svo að plex sýni þáttinn fallega og gerir filename "${SERÍA} - S01E${EPISODE} - ${EPISODE_NAME}" 49 | 50 | Rúv api-inn er ekkert æðislegur, stundum er sami þáttur á 2 mismunandi ID's og þættir geta haft kolröng númer (númer hvað þátturinn er í seríunni), til að koma til móts við það bætti ég við "plexify_clash" og þá í staðinn fyrir að eiga 2 þætti sem eru báður "s01e12" þá verður annar þeirra "s01e112", þarf að bæta þessa logík ...... 51 | 52 | 53 | held að config.ini_example sé nokkuð solid base fyrir flesta 54 | 55 | # Stuff sem ég bæti kannski við, ef ég nenni 56 | .... eða eins og sumir kalla "to do" 57 | 58 | - hafa eitthvað error handling 59 | - gera basic search function fyrir þætti 60 | - breyta path handling úr string cat yfir í os.path.join functions 61 | 62 | 63 | # Credits: 64 | Hefði aldrei nennt að henda þessu saman ef ég hefði ekki haft hinn fínt kommentaða kóða [ruvsarpur](https://github.com/sverrirs/ruvsarpur) eftir [sverrirs](https://github.com/sverrirs) til að renna yfir og byggja API köllin á 65 | 66 | 67 | -------------------------------------------------------------------------------- /ruv_downloader.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | import json 4 | import dbm 5 | import os 6 | import pathlib 7 | import sys 8 | import configparser 9 | import urllib.request 10 | 11 | 12 | 13 | class colors: 14 | 15 | 16 | reset = '\033[0m' 17 | bold = '\033[01m' 18 | disable = '\033[02m' 19 | underline = '\033[04m' 20 | reverse = '\033[07m' 21 | strikethrough = '\033[09m' 22 | invisible = '\033[08m' 23 | 24 | class fg: 25 | black = '\033[30m' 26 | red = '\033[31m' 27 | green = '\033[32m' 28 | orange = '\033[33m' 29 | blue = '\033[34m' 30 | purple = '\033[35m' 31 | cyan = '\033[36m' 32 | lightgrey = '\033[37m' 33 | darkgrey = '\033[90m' 34 | lightred = '\033[91m' 35 | lightgreen = '\033[92m' 36 | yellow = '\033[93m' 37 | lightblue = '\033[94m' 38 | pink = '\033[95m' 39 | lightcyan = '\033[96m' 40 | 41 | class bg: 42 | black = '\033[40m' 43 | red = '\033[41m' 44 | green = '\033[42m' 45 | orange = '\033[43m' 46 | blue = '\033[44m' 47 | purple = '\033[45m' 48 | cyan = '\033[46m' 49 | lightgrey = '\033[47m' 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) 58 | dbm_file = script_dir + "/.ruvdata" 59 | config_file = script_dir + "/config.ini" 60 | kvs = dbm.open(dbm_file, 'c') 61 | cache_expiry = 3600 62 | 63 | 64 | config = configparser.ConfigParser() 65 | config.read(config_file) 66 | 67 | 68 | COLOR_PRINT = False 69 | PLEXIFY = False 70 | PLEXIFY_CLASHES = False 71 | 72 | DEBUG = False 73 | DISABLE_M3U8_OUTPUT = False 74 | 75 | 76 | if "config" in config: 77 | if "colors" in config["config"]: 78 | if config["config"]["colors"].lower() == "true": 79 | COLOR_PRINT = True 80 | if "plexify" in config["config"]: 81 | if config["config"]["plexify"].lower() == "true": 82 | PLEXIFY = True 83 | if "plexify_clashes" in config["config"]: 84 | if config["config"]["plexify_clashes"].lower() == "true": 85 | PLEXIFY_CLASHES = True 86 | 87 | if "debug" in config["config"]: 88 | if config["config"]["debug"].lower() == "true": 89 | DEBUG = True 90 | elif config["config"]["debug"].lower() == "false": 91 | DEBUG = False 92 | else: 93 | print("!! debug in [config] has an invalid value") 94 | 95 | if "disable_m3u8_output" in config["config"]: 96 | 97 | if config["config"]["disable_m3u8_output"].lower() == "true": 98 | DISABLE_M3U8_OUTPUT = True 99 | elif config["config"]["disable_m3u8_output"].lower() == "false": 100 | DISABLE_M3U8_OUTPUT = False 101 | 102 | else: 103 | print("!! disable_m3u8_output in [config] has an invalid value") 104 | 105 | 106 | if sys.platform in [ "linux", "darwin" ]: 107 | import shutil 108 | if "config" in config: 109 | if "ffmpeg_path" in config["config"]: 110 | ffmpeg_path = config["config"]["ffmpeg_path"] 111 | if os.path.isdir(ffmpeg_path): 112 | #sys.path.append(ffmpeg_path) 113 | os.environ['PATH'] += ':' + ffmpeg_path 114 | else: 115 | print("Error: ffmpeg path defined but is not a directory") 116 | exit() 117 | 118 | 119 | 120 | ffmpeg_which = shutil.which("ffmpeg") 121 | if ffmpeg_which == None: 122 | print("Error: can not find ffmpeg in path") 123 | exit() 124 | 125 | 126 | 127 | 128 | try: 129 | import m3u8_To_MP4 130 | except: 131 | print("Install m3u8_To_MP4") 132 | print("pip install m3u8_To_MP4") 133 | exit() 134 | 135 | 136 | if not DEBUG: 137 | m3u8_To_MP4.logging.disable() 138 | 139 | def pprint(msg): 140 | try: 141 | print(json.dumps(msg,indent=2)) 142 | except: 143 | print(msg) 144 | 145 | def debug(msg): 146 | if DEBUG: 147 | print(msg) 148 | 149 | 150 | def colorPrint(status,show,episode,underline=False): 151 | 152 | pad = [ 35, 35, 35 ] 153 | 154 | if COLOR_PRINT: 155 | 156 | if underline: 157 | ul = colors.underline 158 | else: 159 | ul = colors.reset 160 | 161 | if status == "Line": 162 | print(colors.bg.lightgrey,colors.fg.black,"|".ljust(pad[0],"-"),"|".ljust(pad[1],"-"),"|".ljust(pad[2],"-"),colors.reset) 163 | return 164 | 165 | rs = colors.reset 166 | 167 | if status == "[Episode already downloaded]": 168 | bg = colors.bg.blue 169 | fg = colors.fg.lightgrey 170 | elif status == "[Downloading episode]": 171 | bg = colors.bg.green 172 | fg = colors.fg.black 173 | elif status == "[Downloaded episode]": 174 | bg = colors.bg.green 175 | fg = colors.fg.black 176 | elif status == "[Not Downloading episode]": 177 | bg = colors.bg.red 178 | fg = colors.fg.lightgrey 179 | elif status == "Status": 180 | bg = colors.bg.lightgrey 181 | fg = colors.fg.black 182 | else: 183 | bg = colors.bg.black 184 | fg = colors.fg.orange 185 | 186 | print(ul,bg,fg,status.ljust(pad[0]),show.ljust(pad[1]),episode.ljust(pad[2]),rs) 187 | 188 | 189 | def fetchShowList(): 190 | 191 | url = "https://api.ruv.is/api/programs/tv/all" 192 | response = requests.request("GET", url) 193 | 194 | if response.status_code == 200: 195 | 196 | ruv_data = response.json() 197 | 198 | data = { "time" : int(time.time()), 199 | "data" : ruv_data 200 | } 201 | 202 | kvs["showList"] = json.dumps(data) 203 | else: 204 | print("fetchShowList() unsuccessfull") 205 | print("http response code was: " + str(response.status_code)) 206 | print("Exiting") 207 | exit(1) 208 | 209 | 210 | def listShowIds(): 211 | 212 | if "showList" not in kvs: 213 | fetchShowList() 214 | 215 | 216 | showList = json.loads(kvs["showList"]) 217 | showList_age = time.time() - showList["time"] 218 | 219 | if showList_age > cache_expiry: 220 | debug("Showlist is old, fetching new list") 221 | fetchShowList() 222 | showList = json.loads(kvs["showList"]) 223 | else: 224 | debug("Using cached showlist") 225 | 226 | showList_data = showList["data"] 227 | 228 | showList_pad = [ 10, 70, 10 ] 229 | 230 | print("ID".ljust(showList_pad[0]) + "TITLE".ljust(showList_pad[1]) + "AVAIL".ljust(showList_pad[2])) 231 | 232 | for entry in showList_data: 233 | id = str(entry["id"]) 234 | title = entry["title"] 235 | avail = str(entry["web_available_episodes"]) 236 | print(id.ljust(showList_pad[0]) + "|" + title.ljust(showList_pad[1]) + "|" + avail.ljust(showList_pad[2])) 237 | 238 | def fetchEpisodeList(show_id): 239 | 240 | show_id = str(show_id) 241 | 242 | #base_url = "https://api.ruv.is/api/programs/get_ids/" 243 | #url = base_url + show_id 244 | 245 | base_url = "https://api.ruv.is/api/programs/program/SHOW_ID/all" 246 | url = base_url.replace("SHOW_ID",show_id) 247 | 248 | response = requests.request("GET", url) 249 | 250 | if response.status_code == 200: 251 | show_data = response.json() 252 | 253 | data = { "time" : int(time.time()), 254 | "data" : show_data 255 | } 256 | 257 | key = "show_" + show_id 258 | 259 | kvs[key] = json.dumps(data) 260 | 261 | 262 | 263 | def listEpisodes(show_id): 264 | 265 | show_id = str(show_id) 266 | 267 | show_kvs_key = "show_" + show_id 268 | 269 | 270 | if show_kvs_key in kvs: 271 | 272 | debug("show list exists") 273 | show_data = json.loads(kvs[show_kvs_key]) 274 | show_list_age = time.time() - show_data["time"] 275 | 276 | if show_list_age > cache_expiry: 277 | debug("Episode list is old, getting new one") 278 | fetchEpisodeList(show_id) 279 | show_data = json.loads(kvs[show_kvs_key]) 280 | else: 281 | debug("Using cached episode list") 282 | 283 | else: 284 | debug("Show list does not exist, downloading list") 285 | fetchEpisodeList(show_id) 286 | show_data = json.loads(kvs[show_kvs_key]) 287 | 288 | 289 | return show_data["data"] 290 | 291 | 292 | 293 | def checkIfFileExists(url,directory,filename,friendly_name,plexify,force=False,dryrun=False,underline=False): 294 | 295 | 296 | if not os.path.isdir(directory): 297 | print("Directory: " + directory + " does not exist") 298 | return 299 | 300 | file_name_mp4 = filename + ".mp4" 301 | 302 | output_file = os.path.join(directory,file_name_mp4) 303 | 304 | dl_msg_pad = 35 305 | 306 | if os.path.isfile(output_file): 307 | file_exists = True 308 | else: 309 | file_exists = False 310 | 311 | 312 | if force: 313 | file_exists = False 314 | 315 | return file_exists 316 | 317 | def downloadEpisode(url,directory,filename,friendly_name,plexify,force=False,dryrun=False,underline=False): 318 | 319 | 320 | success = True 321 | 322 | if dryrun: 323 | colorPrint("[Not Downloading episode]",friendly_name[0],friendly_name[1],underline) 324 | else: 325 | if DISABLE_M3U8_OUTPUT: 326 | colorPrint("[Downloading episode]",friendly_name[0],friendly_name[1],underline) 327 | sys.stdout = open(os.devnull, 'w') 328 | 329 | try: 330 | m3u8_To_MP4.multithread_download(url,mp4_file_dir=directory,mp4_file_name=filename) 331 | except: 332 | success = False 333 | 334 | 335 | if DISABLE_M3U8_OUTPUT: 336 | sys.stdout = sys.__stdout__ 337 | else: 338 | colorPrint("[Downloaded episode]",friendly_name[0],friendly_name[1],underline) 339 | 340 | return success 341 | 342 | def kvsCheckIfDownloaded(show_id,episode_id): 343 | 344 | 345 | if show_id in kvs: 346 | show_kvs_data = json.loads(kvs[show_id]) 347 | 348 | if episode_id in show_kvs_data: 349 | return True 350 | else: 351 | return False 352 | 353 | else: 354 | return False 355 | 356 | def kvsRegisterDownload(show_id,episode_id): 357 | 358 | if show_id in kvs: 359 | show_kvs_data = json.loads(kvs[show_id]) 360 | else: 361 | show_kvs_data = { } 362 | 363 | show_kvs_data[episode_id] = True 364 | 365 | kvs[show_id] = json.dumps(show_kvs_data) 366 | 367 | 368 | 369 | 370 | def autoDownload(): 371 | 372 | force = False 373 | dryrun = False 374 | underline = False 375 | 376 | if "autodownload" not in config: 377 | print("autodownload not configured") 378 | exit(1) 379 | 380 | 381 | colorPrint("Status","Show","Episode") 382 | colorPrint("Line","","") 383 | 384 | for entry in config["autodownload"]: 385 | active = config["autodownload"][entry] 386 | if active.lower() == "true": 387 | 388 | entry_config = config[entry] 389 | show_id = entry_config["show_id"] 390 | dl_dir = entry_config["dl_dir"] 391 | path = pathlib.Path(dl_dir) 392 | path.mkdir(parents=True, exist_ok=True) 393 | 394 | 395 | show_data = listEpisodes(show_id) 396 | 397 | show_name_slug = show_data["slug"] 398 | show_name = show_data["title"] 399 | 400 | 401 | 402 | episodes = show_data["episodes"] 403 | 404 | if "plexify" in config[entry]: 405 | if config[entry]["plexify"].lower() == "true": 406 | plexify = True 407 | else: 408 | plexify = False 409 | else: 410 | plexify = PLEXIFY 411 | 412 | if "plexify_clashes" in config[entry]: 413 | if config[entry]["plexify"].lower() == "true": 414 | plexify_clashes = True 415 | else: 416 | plexify_clashes = False 417 | else: 418 | plexify_clashes = PLEXIFY_CLASHES 419 | 420 | 421 | 422 | if plexify: 423 | 424 | plex_image_path = os.path.join(dl_dir, "show.jpg") 425 | if not os.path.isfile(plex_image_path): 426 | base_image_url = show_data["image"] 427 | image_url_hq = base_image_url.replace("480x","1920x").replace("quality(65)","quality(100)") 428 | data = urllib.request.urlretrieve(image_url_hq,plex_image_path) 429 | 430 | 431 | 432 | 433 | 434 | 435 | for episode in episodes: 436 | 437 | episode_name_slug = episode["slug"] 438 | episode_name = episode["title"] 439 | episode_url = episode["file"] 440 | 441 | 442 | episode_number = episode["number"] 443 | 444 | 445 | kvs_show_id = show_name_slug + "-" + show_id 446 | kvs_episode_id = episode_name_slug 447 | 448 | 449 | if plexify_clashes: 450 | files_in_dir = os.listdir(dl_dir) 451 | 452 | 453 | for entry in files_in_dir: 454 | if episode_name not in entry: 455 | if "s01e" + str(episode_number) + " " in entry: 456 | episode_number = "1" + str(episode_number) 457 | 458 | 459 | 460 | 461 | friendly_name = [show_name,episode_name] 462 | 463 | 464 | if plexify: 465 | file_name = show_name + " - s01e" + str(episode_number) + " - " + episode_name 466 | 467 | else: 468 | file_name = show_name_slug + "_" + episode_name_slug 469 | 470 | show_downloaded = False 471 | 472 | 473 | if kvsCheckIfDownloaded(kvs_show_id,kvs_episode_id): 474 | colorPrint("[Episode already downloaded]",friendly_name[0],friendly_name[1],underline) 475 | 476 | elif checkIfFileExists(episode_url,dl_dir,file_name,friendly_name,plexify,force,dryrun,underline): 477 | colorPrint("[Episode already downloaded]",friendly_name[0],friendly_name[1],underline) 478 | 479 | # til að samræma downloads áður en KVS management dótið kom í gagnið 480 | kvsRegisterDownload(kvs_show_id,kvs_episode_id) 481 | 482 | else: 483 | success = downloadEpisode(episode_url,dl_dir,file_name,friendly_name,plexify,force,dryrun,underline) 484 | if success: 485 | kvsRegisterDownload(kvs_show_id,kvs_episode_id) 486 | 487 | underline = not underline 488 | 489 | 490 | 491 | 492 | 493 | def manage_show_kvs(action,show_name="null",episode_name="null"): 494 | 495 | 496 | 497 | if action == "list_shows": 498 | 499 | for entry in kvs.keys(): 500 | entry = entry.decode() 501 | 502 | if entry == "showList": 503 | pass 504 | elif entry.startswith("show_"): 505 | pass 506 | else: 507 | print(entry) 508 | 509 | elif action == "list_episodes": 510 | if show_name not in kvs: 511 | print("Show not found in kvs") 512 | else: 513 | episode_list = json.loads(kvs[show_name]) 514 | print("KVS entries for show: " + show_name) 515 | for episode in episode_list: 516 | print(episode) 517 | 518 | 519 | elif action == "delete_show": 520 | 521 | if show_name not in kvs: 522 | print("Show not found in kvs") 523 | else: 524 | print("Deleting kvs entries for: " + show_name) 525 | del kvs[show_name] 526 | 527 | elif action == "delete_episode": 528 | if show_name not in kvs: 529 | print("Show not found in kvs") 530 | else: 531 | episode_list = json.loads(kvs[show_name]) 532 | 533 | if episode_name not in episode_list: 534 | print("Episode not found in kvs") 535 | else: 536 | print("Removing episode: " + episode_name + " from show: " + show_name) 537 | del episode_list[episode_name] 538 | kvs[show_name] = json.dumps(episode_list) 539 | 540 | 541 | elif action == "delete_all": 542 | for entry in kvs.keys(): 543 | entry = entry.decode() 544 | 545 | if entry == "showList": 546 | pass 547 | elif entry.startswith("show_"): 548 | pass 549 | else: 550 | del kvs[entry] 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | def parseArgs(): 560 | 561 | 562 | help_msg="\n usage is: \n " + sys.argv[0] + " \n\n list: Lists all show names and their ID \n auto: downloads files according to config.ini \n\n\n The KVS command manages the list of downloaded files, it only deals with the download records, not the files themselves\n kvs [list_shows|list_episodes|delete_show|delete_episode|delete_all] \n kvs list_shows # lists shows \n kvs list_episodes {show_name} # lists episodes of a show\n kvs delete_show {show_name} # Delete all records of a show \n kvs delete_episode {show_name} {episode_name} # delete the record of a single episode \n kvs delete_all # .... deletes all records \n\n\n help: prints this not-great help message\n" 563 | 564 | if len(sys.argv) == 1: 565 | print(help_msg) 566 | exit(0) 567 | 568 | if "help" in str(sys.argv).lower(): 569 | print(help_msg) 570 | exit(0) 571 | 572 | action = sys.argv[1] 573 | 574 | if "auto" in action.lower(): 575 | autoDownload() 576 | exit(0) 577 | 578 | if "list" in action.lower(): 579 | listShowIds() 580 | exit(0) 581 | 582 | if "kvs" in action.lower(): 583 | 584 | kvs_args = { 585 | "kvs_action" : "none", 586 | "show_name" : "none", 587 | "episode_name" : "none" 588 | } 589 | 590 | kvs_options = [ "script", "action","kvs_action","show_name","episode_name" ] 591 | 592 | if len(sys.argv) == 2: 593 | print(help_msg) 594 | exit() 595 | 596 | arg_counter = 0 597 | for entry in sys.argv: 598 | kvs_args[kvs_options[arg_counter]] = entry 599 | arg_counter += 1 600 | 601 | 602 | 603 | manage_show_kvs(kvs_args["kvs_action"],kvs_args["show_name"],kvs_args["episode_name"]) 604 | 605 | 606 | 607 | 608 | exit(0) 609 | 610 | 611 | 612 | 613 | print("Action: " + action + " is not defined") 614 | print(help_msg) 615 | exit(1) 616 | 617 | 618 | 619 | if __name__ == '__main__': 620 | parseArgs() 621 | 622 | 623 | -------------------------------------------------------------------------------- /krakkaruv_skitafix.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | import json 4 | import dbm 5 | import os 6 | import pathlib 7 | import sys 8 | import configparser 9 | import urllib.request 10 | 11 | 12 | 13 | class colors: 14 | 15 | 16 | reset = '\033[0m' 17 | bold = '\033[01m' 18 | disable = '\033[02m' 19 | underline = '\033[04m' 20 | reverse = '\033[07m' 21 | strikethrough = '\033[09m' 22 | invisible = '\033[08m' 23 | 24 | class fg: 25 | black = '\033[30m' 26 | red = '\033[31m' 27 | green = '\033[32m' 28 | orange = '\033[33m' 29 | blue = '\033[34m' 30 | purple = '\033[35m' 31 | cyan = '\033[36m' 32 | lightgrey = '\033[37m' 33 | darkgrey = '\033[90m' 34 | lightred = '\033[91m' 35 | lightgreen = '\033[92m' 36 | yellow = '\033[93m' 37 | lightblue = '\033[94m' 38 | pink = '\033[95m' 39 | lightcyan = '\033[96m' 40 | 41 | class bg: 42 | black = '\033[40m' 43 | red = '\033[41m' 44 | green = '\033[42m' 45 | orange = '\033[43m' 46 | blue = '\033[44m' 47 | purple = '\033[45m' 48 | cyan = '\033[46m' 49 | lightgrey = '\033[47m' 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) 58 | dbm_file = script_dir + "/.ruvdata" 59 | config_file = script_dir + "/config.ini" 60 | kvs = dbm.open(dbm_file, 'c') 61 | cache_expiry = 3600 62 | 63 | 64 | config = configparser.ConfigParser() 65 | config.read(config_file) 66 | 67 | 68 | COLOR_PRINT = False 69 | PLEXIFY = False 70 | PLEXIFY_CLASHES = False 71 | 72 | DEBUG = False 73 | DISABLE_M3U8_OUTPUT = False 74 | 75 | 76 | if "config" in config: 77 | if "colors" in config["config"]: 78 | if config["config"]["colors"].lower() == "true": 79 | COLOR_PRINT = True 80 | if "plexify" in config["config"]: 81 | if config["config"]["plexify"].lower() == "true": 82 | PLEXIFY = True 83 | if "plexify_clashes" in config["config"]: 84 | if config["config"]["plexify_clashes"].lower() == "true": 85 | PLEXIFY_CLASHES = True 86 | 87 | if "debug" in config["config"]: 88 | if config["config"]["debug"].lower() == "true": 89 | DEBUG = True 90 | elif config["config"]["debug"].lower() == "false": 91 | DEBUG = False 92 | else: 93 | print("!! debug in [config] has an invalid value") 94 | 95 | if "disable_m3u8_output" in config["config"]: 96 | 97 | if config["config"]["disable_m3u8_output"].lower() == "true": 98 | DISABLE_M3U8_OUTPUT = True 99 | elif config["config"]["disable_m3u8_output"].lower() == "false": 100 | DISABLE_M3U8_OUTPUT = False 101 | 102 | else: 103 | print("!! disable_m3u8_output in [config] has an invalid value") 104 | 105 | 106 | if sys.platform in [ "linux", "darwin" ]: 107 | import shutil 108 | if "config" in config: 109 | if "ffmpeg_path" in config["config"]: 110 | ffmpeg_path = config["config"]["ffmpeg_path"] 111 | if os.path.isdir(ffmpeg_path): 112 | #sys.path.append(ffmpeg_path) 113 | os.environ['PATH'] += ':' + ffmpeg_path 114 | else: 115 | print("Error: ffmpeg path defined but is not a directory") 116 | exit() 117 | 118 | 119 | 120 | ffmpeg_which = shutil.which("ffmpeg") 121 | if ffmpeg_which == None: 122 | print("Error: can not find ffmpeg in path") 123 | exit() 124 | 125 | 126 | 127 | 128 | try: 129 | import m3u8_To_MP4 130 | except: 131 | print("Install m3u8_To_MP4") 132 | print("pip install m3u8_To_MP4") 133 | exit() 134 | 135 | 136 | if not DEBUG: 137 | m3u8_To_MP4.logging.disable() 138 | 139 | def pprint(msg): 140 | try: 141 | print(json.dumps(msg,indent=2)) 142 | except: 143 | print(msg) 144 | 145 | def debug(msg): 146 | if DEBUG: 147 | print(msg) 148 | 149 | 150 | def colorPrint(status,show,episode,underline=False): 151 | 152 | pad = [ 35, 35, 35 ] 153 | 154 | if COLOR_PRINT: 155 | 156 | if underline: 157 | ul = colors.underline 158 | else: 159 | ul = colors.reset 160 | 161 | if status == "Line": 162 | print(colors.bg.lightgrey,colors.fg.black,"|".ljust(pad[0],"-"),"|".ljust(pad[1],"-"),"|".ljust(pad[2],"-"),colors.reset) 163 | return 164 | 165 | rs = colors.reset 166 | 167 | if status == "[Episode already downloaded]": 168 | bg = colors.bg.blue 169 | fg = colors.fg.lightgrey 170 | elif status == "[Downloading episode]": 171 | bg = colors.bg.green 172 | fg = colors.fg.black 173 | elif status == "[Downloaded episode]": 174 | bg = colors.bg.green 175 | fg = colors.fg.black 176 | elif status == "[Not Downloading episode]": 177 | bg = colors.bg.red 178 | fg = colors.fg.lightgrey 179 | elif status == "Status": 180 | bg = colors.bg.lightgrey 181 | fg = colors.fg.black 182 | else: 183 | bg = colors.bg.black 184 | fg = colors.fg.orange 185 | 186 | print(ul,bg,fg,status.ljust(pad[0]),show.ljust(pad[1]),episode.ljust(pad[2]),rs) 187 | 188 | 189 | def fetchShowList(): 190 | 191 | #url = "https://api.ruv.is/api/programs/tv/all" 192 | 193 | 194 | temp_file = ".tmp_file" 195 | 196 | file_age = os.path.getmtime(temp_file) 197 | time_now = time.time() 198 | 199 | file_delta = time_now - file_age 200 | 201 | if file_delta > 3600: 202 | 203 | 204 | url = "https://pico-server.spilari.ruv.is/programs/featured/krakkaruv" 205 | response = requests.request("GET", url) 206 | 207 | 208 | f = open(temp_file,"wb") 209 | f.write(response.content) 210 | 211 | x = open(temp_file).read() 212 | y = json.loads(x) 213 | 214 | 215 | shows = { } 216 | shows["id"] = { } 217 | shows["slug"] = { } 218 | 219 | for entry in y["panels"]: 220 | 221 | for n in entry["programs"]: 222 | 223 | show_slug = n["slug"] 224 | show_id = n["id"] 225 | 226 | if show_slug not in shows["slug"]: 227 | shows["slug"][show_slug] = { } 228 | shows["slug"][show_slug]["data"] = n 229 | shows["slug"][show_slug]["list"] = [ ] 230 | if show_id not in shows["id"]: 231 | shows["id"][show_id] = { } 232 | shows["id"][show_id]["data"] = n 233 | shows["id"][show_id]["list"] = [ ] 234 | 235 | for episode in n["episodes"]: 236 | shows["slug"][show_slug]["list"].append(episode) 237 | shows["id"][show_id]["list"].append(episode) 238 | 239 | 240 | kvs["shitfix"] = json.dumps(shows) 241 | 242 | 243 | 244 | def listShowIds(): 245 | 246 | if "showList" not in kvs: 247 | fetchShowList() 248 | 249 | 250 | showList = json.loads(kvs["showList"]) 251 | showList_age = time.time() - showList["time"] 252 | 253 | if showList_age > cache_expiry: 254 | debug("Showlist is old, fetching new list") 255 | fetchShowList() 256 | showList = json.loads(kvs["showList"]) 257 | else: 258 | debug("Using cached showlist") 259 | 260 | showList_data = showList["data"] 261 | 262 | showList_pad = [ 10, 70, 10 ] 263 | 264 | print("ID".ljust(showList_pad[0]) + "TITLE".ljust(showList_pad[1]) + "AVAIL".ljust(showList_pad[2])) 265 | 266 | for entry in showList_data: 267 | id = str(entry["id"]) 268 | title = entry["title"] 269 | avail = str(entry["web_available_episodes"]) 270 | print(id.ljust(showList_pad[0]) + "|" + title.ljust(showList_pad[1]) + "|" + avail.ljust(showList_pad[2])) 271 | 272 | def fetchEpisodeList(show_id): 273 | 274 | show_id = str(show_id) 275 | 276 | #base_url = "https://api.ruv.is/api/programs/get_ids/" 277 | #url = base_url + show_id 278 | 279 | base_url = "https://api.ruv.is/api/programs/program/SHOW_ID/all" 280 | url = base_url.replace("SHOW_ID",show_id) 281 | 282 | response = requests.request("GET", url) 283 | 284 | if response.status_code == 200: 285 | show_data = response.json() 286 | 287 | data = { "time" : int(time.time()), 288 | "data" : show_data 289 | } 290 | 291 | key = "show_" + show_id 292 | 293 | kvs[key] = json.dumps(data) 294 | 295 | 296 | 297 | def listEpisodes(show_id): 298 | 299 | show_id = str(show_id) 300 | 301 | show_kvs_key = "show_" + show_id 302 | 303 | 304 | if show_kvs_key in kvs: 305 | 306 | debug("show list exists") 307 | show_data = json.loads(kvs[show_kvs_key]) 308 | show_list_age = time.time() - show_data["time"] 309 | 310 | if show_list_age > cache_expiry: 311 | debug("Episode list is old, getting new one") 312 | fetchEpisodeList(show_id) 313 | show_data = json.loads(kvs[show_kvs_key]) 314 | else: 315 | debug("Using cached episode list") 316 | 317 | else: 318 | debug("Show list does not exist, downloading list") 319 | fetchEpisodeList(show_id) 320 | show_data = json.loads(kvs[show_kvs_key]) 321 | 322 | 323 | return show_data["data"] 324 | 325 | 326 | 327 | def checkIfFileExists(url,directory,filename,friendly_name,plexify,force=False,dryrun=False,underline=False): 328 | 329 | 330 | if not os.path.isdir(directory): 331 | print("Directory: " + directory + " does not exist") 332 | return 333 | 334 | file_name_mp4 = filename + ".mp4" 335 | 336 | output_file = os.path.join(directory,file_name_mp4) 337 | 338 | dl_msg_pad = 35 339 | 340 | if os.path.isfile(output_file): 341 | file_exists = True 342 | else: 343 | file_exists = False 344 | 345 | 346 | if force: 347 | file_exists = False 348 | 349 | return file_exists 350 | 351 | def downloadEpisode(url,directory,filename,friendly_name,plexify,force=False,dryrun=False,underline=False): 352 | 353 | 354 | success = True 355 | 356 | if dryrun: 357 | colorPrint("[Not Downloading episode]",friendly_name[0],friendly_name[1],underline) 358 | else: 359 | if DISABLE_M3U8_OUTPUT: 360 | colorPrint("[Downloading episode]",friendly_name[0],friendly_name[1],underline) 361 | sys.stdout = open(os.devnull, 'w') 362 | 363 | try: 364 | m3u8_To_MP4.multithread_download(url,mp4_file_dir=directory,mp4_file_name=filename) 365 | except: 366 | success = False 367 | 368 | 369 | if DISABLE_M3U8_OUTPUT: 370 | sys.stdout = sys.__stdout__ 371 | else: 372 | colorPrint("[Downloaded episode]",friendly_name[0],friendly_name[1],underline) 373 | 374 | return success 375 | 376 | def kvsCheckIfDownloaded(show_id,episode_id): 377 | 378 | 379 | if show_id in kvs: 380 | show_kvs_data = json.loads(kvs[show_id]) 381 | 382 | if episode_id in show_kvs_data: 383 | return True 384 | else: 385 | return False 386 | 387 | else: 388 | return False 389 | 390 | def kvsRegisterDownload(show_id,episode_id): 391 | 392 | if show_id in kvs: 393 | show_kvs_data = json.loads(kvs[show_id]) 394 | else: 395 | show_kvs_data = { } 396 | 397 | show_kvs_data[episode_id] = True 398 | 399 | kvs[show_id] = json.dumps(show_kvs_data) 400 | 401 | 402 | 403 | 404 | def autoDownload(): 405 | 406 | force = False 407 | dryrun = False 408 | underline = False 409 | 410 | if "autodownload" not in config: 411 | print("autodownload not configured") 412 | exit(1) 413 | 414 | 415 | colorPrint("Status","Show","Episode") 416 | colorPrint("Line","","") 417 | 418 | 419 | for entry in config["autodownload"]: 420 | active = config["autodownload"][entry] 421 | if active.lower() == "true": 422 | 423 | entry_config = config[entry] 424 | show_id = entry_config["show_id"] 425 | dl_dir = entry_config["dl_dir"] 426 | path = pathlib.Path(dl_dir) 427 | path.mkdir(parents=True, exist_ok=True) 428 | 429 | 430 | #show_data = listEpisodes(show_id) 431 | nn = kvs["shitfix"].decode() 432 | sd = json.loads(nn) 433 | show_data = sd["id"][show_id]["data"] 434 | #show_data = kvs["shitfix"]["id"]["show_id"]["data"] 435 | 436 | 437 | 438 | show_name_slug = show_data["slug"] 439 | show_name = show_data["title"] 440 | 441 | 442 | 443 | episodes = sd["id"][show_id]["list"] 444 | 445 | #episodes = show_data["episodes"] 446 | 447 | if "plexify" in config[entry]: 448 | if config[entry]["plexify"].lower() == "true": 449 | plexify = True 450 | else: 451 | plexify = False 452 | else: 453 | plexify = PLEXIFY 454 | 455 | if "plexify_clashes" in config[entry]: 456 | if config[entry]["plexify"].lower() == "true": 457 | plexify_clashes = True 458 | else: 459 | plexify_clashes = False 460 | else: 461 | plexify_clashes = PLEXIFY_CLASHES 462 | 463 | 464 | 465 | if plexify: 466 | 467 | plex_image_path = os.path.join(dl_dir, "show.jpg") 468 | if not os.path.isfile(plex_image_path): 469 | base_image_url = show_data["image"] 470 | image_url_hq = base_image_url.replace("480x","1920x").replace("quality(65)","quality(100)") 471 | data = urllib.request.urlretrieve(image_url_hq,plex_image_path) 472 | 473 | 474 | 475 | 476 | 477 | 478 | for episode in episodes: 479 | 480 | pprint(episode) 481 | episode_name_slug = episode["title"] 482 | episode_name = episode["title"] 483 | episode_url = episode["file"] 484 | 485 | 486 | episode_number = episode["id"] 487 | en = ''.join(ch for ch in episode_number if ch.isdigit()) 488 | 489 | episode_number = en 490 | #episode_number = episode["number"] 491 | 492 | 493 | kvs_show_id = show_name_slug + "-" + show_id 494 | kvs_episode_id = episode_name_slug 495 | 496 | 497 | if plexify_clashes: 498 | files_in_dir = os.listdir(dl_dir) 499 | 500 | 501 | for entry in files_in_dir: 502 | if episode_name not in entry: 503 | if "s01e" + str(episode_number) + " " in entry: 504 | episode_number = "1" + str(episode_number) 505 | 506 | 507 | 508 | 509 | friendly_name = [show_name,episode_name] 510 | 511 | 512 | if plexify: 513 | file_name = show_name + " - s01e" + str(episode_number) + " - " + episode_name 514 | 515 | else: 516 | file_name = show_name_slug + "_" + episode_name_slug 517 | 518 | show_downloaded = False 519 | 520 | 521 | if kvsCheckIfDownloaded(kvs_show_id,kvs_episode_id): 522 | colorPrint("[Episode already downloaded]",friendly_name[0],friendly_name[1],underline) 523 | 524 | elif checkIfFileExists(episode_url,dl_dir,file_name,friendly_name,plexify,force,dryrun,underline): 525 | colorPrint("[Episode already downloaded]",friendly_name[0],friendly_name[1],underline) 526 | 527 | # til að samræma downloads áður en KVS management dótið kom í gagnið 528 | kvsRegisterDownload(kvs_show_id,kvs_episode_id) 529 | 530 | else: 531 | success = downloadEpisode(episode_url,dl_dir,file_name,friendly_name,plexify,force,dryrun,underline) 532 | if success: 533 | kvsRegisterDownload(kvs_show_id,kvs_episode_id) 534 | 535 | underline = not underline 536 | 537 | 538 | 539 | 540 | 541 | def manage_show_kvs(action,show_name="null",episode_name="null"): 542 | 543 | 544 | 545 | if action == "list_shows": 546 | 547 | for entry in kvs.keys(): 548 | entry = entry.decode() 549 | 550 | if entry == "showList": 551 | pass 552 | elif entry.startswith("show_"): 553 | pass 554 | else: 555 | print(entry) 556 | 557 | elif action == "list_episodes": 558 | if show_name not in kvs: 559 | print("Show not found in kvs") 560 | else: 561 | episode_list = json.loads(kvs[show_name]) 562 | print("KVS entries for show: " + show_name) 563 | for episode in episode_list: 564 | print(episode) 565 | 566 | 567 | elif action == "delete_show": 568 | 569 | if show_name not in kvs: 570 | print("Show not found in kvs") 571 | else: 572 | print("Deleting kvs entries for: " + show_name) 573 | del kvs[show_name] 574 | 575 | elif action == "delete_episode": 576 | if show_name not in kvs: 577 | print("Show not found in kvs") 578 | else: 579 | episode_list = json.loads(kvs[show_name]) 580 | 581 | if episode_name not in episode_list: 582 | print("Episode not found in kvs") 583 | else: 584 | print("Removing episode: " + episode_name + " from show: " + show_name) 585 | del episode_list[episode_name] 586 | kvs[show_name] = json.dumps(episode_list) 587 | 588 | 589 | elif action == "delete_all": 590 | for entry in kvs.keys(): 591 | entry = entry.decode() 592 | 593 | if entry == "showList": 594 | pass 595 | elif entry.startswith("show_"): 596 | pass 597 | else: 598 | del kvs[entry] 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | def parseArgs(): 608 | 609 | 610 | help_msg="\n usage is: \n " + sys.argv[0] + " \n\n list: Lists all show names and their ID \n auto: downloads files according to config.ini \n\n\n The KVS command manages the list of downloaded files, it only deals with the download records, not the files themselves\n kvs [list_shows|list_episodes|delete_show|delete_episode|delete_all] \n kvs list_shows # lists shows \n kvs list_episodes {show_name} # lists episodes of a show\n kvs delete_show {show_name} # Delete all records of a show \n kvs delete_episode {show_name} {episode_name} # delete the record of a single episode \n kvs delete_all # .... deletes all records \n\n\n help: prints this not-great help message\n" 611 | 612 | if len(sys.argv) == 1: 613 | print(help_msg) 614 | exit(0) 615 | 616 | if "help" in str(sys.argv).lower(): 617 | print(help_msg) 618 | exit(0) 619 | 620 | action = sys.argv[1] 621 | 622 | if "auto" in action.lower(): 623 | autoDownload() 624 | exit(0) 625 | 626 | if "list" in action.lower(): 627 | listShowIds() 628 | exit(0) 629 | 630 | if "kvs" in action.lower(): 631 | 632 | kvs_args = { 633 | "kvs_action" : "none", 634 | "show_name" : "none", 635 | "episode_name" : "none" 636 | } 637 | 638 | kvs_options = [ "script", "action","kvs_action","show_name","episode_name" ] 639 | 640 | if len(sys.argv) == 2: 641 | print(help_msg) 642 | exit() 643 | 644 | arg_counter = 0 645 | for entry in sys.argv: 646 | kvs_args[kvs_options[arg_counter]] = entry 647 | arg_counter += 1 648 | 649 | 650 | 651 | manage_show_kvs(kvs_args["kvs_action"],kvs_args["show_name"],kvs_args["episode_name"]) 652 | 653 | 654 | 655 | 656 | exit(0) 657 | 658 | 659 | 660 | 661 | print("Action: " + action + " is not defined") 662 | print(help_msg) 663 | exit(1) 664 | 665 | def shitfix_list(): 666 | 667 | 668 | x = kvs["shitfix"].decode() 669 | data = json.loads(x) 670 | 671 | maxlen = 0 672 | 673 | for i in data["id"]: 674 | name = data["id"][i]["data"]["title"] 675 | if len(name) > maxlen: 676 | maxlen = len(name) 677 | 678 | for i in data["id"]: 679 | a = data["id"][i]["data"]["title"] 680 | print(a.ljust(maxlen + 5) , i) 681 | 682 | 683 | 684 | def download(show_id): 685 | 686 | pass 687 | 688 | 689 | #if __name__ == '__main__': 690 | #parseArgs() 691 | 692 | #utdownload("1234") 693 | #fetchShowList() 694 | #shitfix_list() 695 | #download("34358") 696 | autoDownload() 697 | --------------------------------------------------------------------------------