├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── RadarrSync.py ├── backup_lidarr_2csv.py ├── backup_radarr_2csv.py ├── backup_readarr_2csv.py ├── backup_sonarr_2csv.py ├── config_example.ini ├── export_trakt.py ├── get_imdbid.py ├── lidarr_add_from_list.py ├── movies.csv ├── radarr_add_from_list.py ├── radarr_remove_downloaded.py ├── radarr_unmonitor_downloaded.py ├── remove_from_sonarr.py ├── requirments.txt ├── show_list.csv ├── shows └── sonarr_add_from_list.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: sirk123au 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | config.ini 3 | *.log 4 | .vscode 5 | trakt_search.py 6 | *.csv 7 | *config.ini 8 | *.pyc 9 | *.csv 10 | import_from_tmdb.py 11 | yt_music_vid_get.py 12 | cookies.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Radarr and Sonarr Tools 2 | 3 | *radarr_add_from_list.py* Add Movies from a csv formatted file. 4 | 5 | *sonarr_add_from_list.py* Add Shows from a csv formatted file. 6 | 7 | *lidarr_add_from_list.py* Add Artists from a csv formatted file. 8 | 9 | *radarr_remove_downloaded.py* Removes already downloaded movies and removes their entries from Radarr. 10 | 11 | *backup_radarr_2csv.py* Creates a backup of the Radarr Database for easy importing. 12 | 13 | *backup_sonarr_2csv.py* Creates a backup of the Sonarr Database for easy importing. 14 | 15 | *backup_lidarr_2csv.py* Creates a backup of the Lidarr Database for easy importing. 16 | 17 | *get_imdbid.py* Matches the imdbd from a csv list MovieName/ShowName,Year for easy importing for the list import. 18 | 19 | Using radarr_add_from_list/sonarr_add_from_list 20 | The input list file has to have the format. 21 | It has to have MovieName/ShowName,Year,imdbid **imdbid is Optional Makes it easer to find movie/TV show 22 | 23 | Use pip install -r requirments.txt to install the required python modules 24 | 25 | Movies CSV 26 | ``` 27 | title,year,imdbid (This header has to be included in the csv for it to work correctly) 28 | Ben-Hur,1959,tt0052618 29 | Gone with the Wind,1939,tt0031381 30 | 31 | Without imdbid 32 | 33 | title,year,imdbid (This header has to be included in the csv for it to work correctly) 34 | The English Patient,1996 35 | Schindler's List,1993 36 | ``` 37 | TV Shows CSV 38 | 39 | ``` 40 | title,year,imdbid (This header has to be included in the csv for it to work correctly) 41 | 13 Reasons Why,2017,tt1837492 42 | 50 Central,2017,tt7261310 43 | 44 | Without imdbID 45 | 46 | title,year,imdbid (This header has to be included in the csv for it to work correctly) 47 | 60 Days In,2016 48 | 9-1-1,2018 49 | A Discovery of Witches,2018 50 | 51 | ``` 52 | Artist CSV 53 | 54 | ``` 55 | artist,foreignArtistId 56 | 10cc 57 | 2 DJ's and One 58 | 3 Doors Down 59 | 3-11 Porter 60 | 61 | With foreignArtistId 62 | 63 | artist,foreignArtistId 64 | Weird Al Yankovic,7746d775-9550-4360-b8d5-c37bd448ce01 65 | Adele,cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493 66 | Alanis Morissette,4bdcee62-4902-4773-8cd1-e252e2e31225 67 | Arctic Monkeys,ada7a83c-e3e1-40f1-93f9-3e73dbc9298a 68 | Augie March,29070ba5-c3df-41d9-bed0-8e2f1e1c22ad 69 | BABYMETAL,27e2997f-f7a1-4353-bcc4-57b9274fa9a4 70 | Backstreet Boys,2f569e60-0a1b-4fb9-95a4-3dc1525d1aad 71 | 72 | ``` 73 | 74 | when you run it use 75 | ``` 76 | $python3 radarr_add_from_list.py movielist.csv 77 | $python3 sonarr_add_from_list.py showlist.csv 78 | $python3 lidarr_add_from_list.py artist.csv 79 | ``` 80 | Rename config_example.ini and add your details 81 | 82 | ``` 83 | [radarr] 84 | api_key = Radarr Api Key 85 | baseurl = http://localhost:7878 Radarr Base Url 86 | urlbase = ; Include URL Base if you have it enabled 87 | rootfolderpath = Movie Root Path (trailing slash is needed eg. /media/Movies/ or d:\media\Movies\) 88 | searchForMovie = Enable forced search 89 | qualityProfileId = 90 | omdbapi_key = sign up here for free api key http://www.omdbapi.com/apikey.aspx 91 | 92 | [sonarr] 93 | api_key = Sonarr Api Key 94 | baseurl = http://localhost:8989 95 | urlbase = ; Include URL Base if you have it enabled 96 | rootfolderpath = Show Root Path (trailing slash is needed eg. /media/shows/ or d:\media\shows\) 97 | searchForShow = 98 | qualityProfileId = 99 | omdbapi_key = 100 | tvdb_api = ; sign up here for a api key https://thetvdb.com/api-information 101 | tvdb_userkey = 102 | tvdb_username = 103 | 104 | [lidarr] 105 | api_key = 106 | baseurl = http://localhost:8686 107 | rootfolderpath = 108 | 109 | 110 | Standard Profile ID 111 | 1 Any 112 | 2 SD 113 | 3 HD-720p 114 | 4 HD-1080p 115 | 5 Ultra-HD 116 | 6 HD - 720p/1080p 117 | ``` 118 | 119 | Thanks for the support :) 120 | 121 | [![Github Sponsorship](https://img.shields.io/badge/support-me-red.svg)](https://github.com/users/sirk123au/sponsorship) 122 | 123 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/M4M25DUMM) 124 | -------------------------------------------------------------------------------- /RadarrSync.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import requests 4 | import json 5 | import configparser 6 | import sys 7 | 8 | 9 | DEV = False 10 | VER = '1.0.1' 11 | 12 | ######################################################################################################################## 13 | logger = logging.getLogger() 14 | logger.setLevel(logging.DEBUG) 15 | logFormatter = logging.Formatter("%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s") 16 | 17 | fileHandler = logging.FileHandler("./Output.txt") 18 | fileHandler.setFormatter(logFormatter) 19 | logger.addHandler(fileHandler) 20 | 21 | consoleHandler = logging.StreamHandler(sys.stdout) 22 | consoleHandler.setFormatter(logFormatter) 23 | logger.addHandler(consoleHandler) 24 | ######################################################################################################################## 25 | 26 | 27 | def ConfigSectionMap(section): 28 | dict1 = {} 29 | options = Config.options(section) 30 | for option in options: 31 | try: 32 | dict1[option] = Config.get(section, option) 33 | if dict1[option] == -1: 34 | logger.debug("skip: %s" % option) 35 | except: 36 | print("exception on %s!" % option) 37 | dict1[option] = None 38 | return dict1 39 | 40 | logger.debug('RadarSync Version {}'.format(VER)) 41 | Config = configparser.ConfigParser() 42 | 43 | # Loads an alternate config file so that I can work on my servers without uploading config to github 44 | if DEV: 45 | settingsFilename = os.path.join(os.getcwd(), 'Dev' 46 | 'Config.txt') 47 | else: 48 | settingsFilename = os.path.join(os.getcwd(), 'Config.txt') 49 | Config.read(settingsFilename) 50 | 51 | radarr_url = ConfigSectionMap("Radarr")['url'] 52 | radarr_key = ConfigSectionMap("Radarr")['key'] 53 | radarrSession = requests.Session() 54 | radarrSession.trust_env = False 55 | radarrMovies = radarrSession.get('{0}/api/movie?apikey={1}'.format(radarr_url, radarr_key)) 56 | if radarrMovies.status_code != 200: 57 | logger.error('Radarr server error - response {}'.format(radarrMovies.status_code)) 58 | sys.exit(0) 59 | 60 | for server in Config.sections(): 61 | 62 | if server == 'Default' or server == "Radarr": 63 | continue # Default section handled previously as it always needed 64 | 65 | else: 66 | logger.debug('syncing to {0}'.format(server)) 67 | 68 | session = requests.Session() 69 | session.trust_env = False 70 | SyncServer_url = ConfigSectionMap(server)['url'] 71 | SyncServer_key = ConfigSectionMap(server)['key'] 72 | SyncServer_target_profile = ConfigSectionMap(server)['target_profile'] 73 | SyncServerMovies = session.get('{0}/api/movie?apikey={1}'.format(SyncServer_url, SyncServer_key)) 74 | if SyncServerMovies.status_code != 200: 75 | logger.error('4K Radarr server error - response {}'.format(SyncServerMovies.status_code)) 76 | sys.exit(0) 77 | 78 | # build a list of movied IDs already in the sync server, this is used later to prevent readding a movie that already 79 | # exists. 80 | # TODO refactor variable names to make it clear this builds list of existing not list of movies to add 81 | # TODO #11 add reconcilliation to remove movies that have been deleted from source server 82 | movieIds_to_syncserver = [] 83 | for movie_to_sync in SyncServerMovies.json(): 84 | movieIds_to_syncserver.append(movie_to_sync['tmdbId']) 85 | #logger.debug('found movie to be added') 86 | 87 | newMovies = 0 88 | searchid = [] 89 | for movie in radarrMovies.json(): 90 | if movie['profileId'] == int(ConfigSectionMap(server)['profile']): 91 | if movie['tmdbId'] not in movieIds_to_syncserver: 92 | logging.debug('title: {0}'.format(movie['title'])) 93 | logging.debug('qualityProfileId: {0}'.format(movie['qualityProfileId'])) 94 | logging.debug('titleSlug: {0}'.format(movie['titleSlug'])) 95 | images = movie['images'] 96 | for image in images: 97 | image['url'] = '{0}{1}'.format(radarr_url, image['url']) 98 | logging.debug(image['url']) 99 | logging.debug('tmdbId: {0}'.format(movie['tmdbId'])) 100 | logging.debug('path: {0}'.format(movie['path'])) 101 | logging.debug('monitored: {0}'.format(movie['monitored'])) 102 | 103 | # Update the path based on "path_from" and "path_to" passed to us in Config.txt 104 | path = movie['path'] 105 | path = path.replace(ConfigSectionMap(server)['path_from'], ConfigSectionMap(server)['path_to']) 106 | 107 | payload = {'title': movie['title'], 108 | 'qualityProfileId': movie['qualityProfileId'], 109 | 'titleSlug': movie['titleSlug'], 110 | 'tmdbId': movie['tmdbId'], 111 | 'path': path, 112 | 'monitored': movie['monitored'], 113 | 'images': images, 114 | 'profileId': SyncServer_target_profile, 115 | 'minimumAvailability': 'released' 116 | } 117 | 118 | r = session.post('{0}/api/movie?apikey={1}'.format(SyncServer_url, SyncServer_key), data=json.dumps(payload)) 119 | if r.status_code == 200: 120 | searchid.append(int(r.json()['id'])) 121 | logger.info('adding {0} to {1} server'.format(movie['title'], server)) 122 | else: 123 | logger.debug('Failed with error: {0}'.format(r.json())) 124 | else: 125 | logging.debug('{0} already in {1} library'.format(movie['title'], server)) 126 | else: 127 | logging.debug('Skipping {0}, wanted profile: {1} found profile: {2}'.format(movie['title'], 128 | movie['profileId'], 129 | int(ConfigSectionMap(server)['profile']) 130 | )) 131 | 132 | 133 | 134 | if len(searchid): 135 | payload = {'name' : 'MoviesSearch', 'movieIds' : searchid} 136 | session.post('{0}/api/command?apikey={1}'.format(SyncServer_url, SyncServer_key), data=json.dumps(payload)) 137 | 138 | -------------------------------------------------------------------------------- /backup_lidarr_2csv.py: -------------------------------------------------------------------------------- 1 | import requests, json, csv, sys, configparser, re 2 | 3 | if sys.version_info[0] < 3: raise Exception("Must be using Python 3") 4 | 5 | config = configparser.ConfigParser() 6 | config.read('./config.ini') 7 | baseurl = config['lidarr']['baseurl'] 8 | api_key = config['lidarr']['api_key'] 9 | 10 | with open('./lidarr_backup.csv', 'w', newline='') as csvfile: 11 | csvwriter = csv.writer(csvfile, delimiter=',') 12 | print("Downloading Data...") 13 | headers = {"Content-type": "application/json", "X-Api-Key": api_key } 14 | url = "{}/api/v1/artist".format(baseurl) 15 | rsp = requests.get(url , headers=headers) 16 | if rsp.status_code == 200: 17 | lidarrData = json.loads(rsp.text) 18 | csvwriter.writerow(['artist','foreignArtistId']) 19 | for d in lidarrData: 20 | artist = re.sub(r'[^a-zA-Z0-9 ]',r'', d['artistName']) 21 | csvwriter.writerow([artist, d.get('foreignArtistId')]) 22 | else: 23 | print("Failed to connect to Radar...") 24 | print("Done...") -------------------------------------------------------------------------------- /backup_radarr_2csv.py: -------------------------------------------------------------------------------- 1 | import requests, json, csv, sys, configparser 2 | 3 | if sys.version_info[0] < 3: raise Exception("Must be using Python 3") 4 | 5 | config = configparser.ConfigParser() 6 | config.read('./config.ini') 7 | baseurl = config['radarr']['baseurl'] 8 | api_key = config['radarr']['api_key'] 9 | urlbase = config['radarr']['urlbase'] 10 | 11 | with open('./radarr_backup.csv', 'w', encoding="utf-8", newline='' ) as csvfile: 12 | csvwriter = csv.writer(csvfile, delimiter=',') 13 | print("Downloading Data...") 14 | headers = {"Content-type": "application/json", "X-Api-Key": api_key } 15 | url = f"{baseurl}{urlbase}/api/v3/movie" 16 | rsp = requests.get(url , headers=headers) 17 | csvwriter.writerow(['title','year','imdbid', 'tmdbId']) 18 | if rsp.status_code == 200: 19 | RadarrData = json.loads(rsp.text) 20 | for d in RadarrData: csvwriter.writerow([d.get('title'),d.get('year'), d.get('imdbId'),d.get('tmdbId')]) 21 | else: 22 | print("Failed to connect to Radar...") 23 | print("Done...") -------------------------------------------------------------------------------- /backup_readarr_2csv.py: -------------------------------------------------------------------------------- 1 | import requests, json, csv, sys, configparser 2 | 3 | if sys.version_info[0] < 3: raise Exception("Must be using Python 3") 4 | 5 | config = configparser.ConfigParser() 6 | config.read('./config.ini') 7 | baseurl = config['readarr']['baseurl'] 8 | api_key = config['readarr']['api_key'] 9 | 10 | with open('./readarr_backup.csv', 'w', newline='') as csvfile: 11 | csvwriter = csv.writer(csvfile, delimiter=',') 12 | print("Downloading Data...") 13 | headers = {"Content-type": "application/json", "X-Api-Key": api_key } 14 | url = "{}/api/v1/book".format(baseurl) 15 | rsp = requests.get(url , headers=headers) 16 | csvwriter.writerow(['AuthorID','AuthorName','BookID','BookName']) 17 | if rsp.status_code == 200: 18 | readarrData = json.loads(rsp.text) 19 | for d in readarrData: csvwriter.writerow([d['author']['foreignAuthorId'], d['author']['authorName'], d['foreignBookId'], d['title']]) 20 | else: 21 | print("Failed to connect to readarr...") 22 | print("Done...") -------------------------------------------------------------------------------- /backup_sonarr_2csv.py: -------------------------------------------------------------------------------- 1 | import requests, json, csv, sys, configparser 2 | 3 | if sys.version_info[0] < 3: raise Exception("Must be using Python 3") 4 | 5 | config = configparser.ConfigParser() 6 | config.read('./config.ini') 7 | baseurl = config['sonarr']['baseurl'] 8 | api_key = config['sonarr']['api_key'] 9 | 10 | with open('./sonarr_backup.csv', 'w', newline='') as csvfile: 11 | csvwriter = csv.writer(csvfile, delimiter=',') 12 | print("Downloading Data...") 13 | headers = {"Content-type": "application/json", "X-Api-Key": api_key } 14 | url = "{}/api/v3/series".format(baseurl) 15 | rsp = requests.get(url , headers=headers) 16 | csvwriter.writerow(['title','year','imdbid']) 17 | if rsp.status_code == 200: 18 | RadarrData = json.loads(rsp.text) 19 | for d in RadarrData: csvwriter.writerow([d['title'],d['year'], d.get('imdbId')]) 20 | else: 21 | print("Failed to connect to Radar...") 22 | print("Done...") -------------------------------------------------------------------------------- /config_example.ini: -------------------------------------------------------------------------------- 1 | [radarr] 2 | api_key = 3 | baseurl = 4 | urlbase = ; Include URL Base if you have it enabled 5 | rootfolderpath = 6 | searchForMovie = 7 | qualityProfileId = 8 | omdbapi_key = ; sign up here for api key http://www.omdbapi.com/apikey.aspx 9 | 10 | [sonarr] 11 | api_key = 12 | baseurl = 13 | urlbase = ; Include URL Base if you have it enabled 14 | rootfolderpath = 15 | searchForShow = 16 | qualityProfileId = 17 | omdbapi_key = 18 | tvdb_api = ; sign up here for a api key https://thetvdb.com/api-information 19 | tvdb_userkey = 20 | tvdb_username = 21 | 22 | [lidarr] 23 | api_key = 24 | baseurl = 25 | rootfolderpath = 26 | 27 | ; Standard Profile ID 28 | ; 1 Any 29 | ; 2 SD 30 | ; 3 HD-720p 31 | ; 4 HD-1080p 32 | ; 5 Ultra-HD 33 | ; 6 HD - 720p/1080p 34 | -------------------------------------------------------------------------------- /export_trakt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # (c) Copyright 2016-2018 xbgmsharp 5 | # 6 | # Purpose: 7 | # Export Movies or TVShows IDs from Trakt.tv 8 | # 9 | # Requirement on Ubuntu/Debian Linux system 10 | # apt-get install python-dateutil python-simplejson python-requests python-openssl jq 11 | # 12 | # Requirement on Windows on Python 2.7 13 | # C:\Python2.7\Scripts\easy_install-2.7.exe simplejson requests 14 | # 15 | 16 | import sys, os 17 | # https://urllib3.readthedocs.org/en/latest/security.html#disabling-warnings 18 | # http://quabr.com/27981545/surpress-insecurerequestwarning-unverified-https-request-is-being-made-in-pytho 19 | # http://docs.python-requests.org/en/v2.4.3/user/advanced/#proxies 20 | try: 21 | import simplejson as json 22 | import requests 23 | requests.packages.urllib3.disable_warnings() 24 | import csv 25 | except: 26 | sys.exit("Please use your favorite mehtod to install the following module requests and simplejson to use this script") 27 | 28 | import argparse 29 | import ConfigParser 30 | import datetime 31 | import collections 32 | import pprint 33 | 34 | pp = pprint.PrettyPrinter(indent=4) 35 | 36 | desc="""This program export Movies or TVShows IDs from Trakt.tv list.""" 37 | 38 | epilog="""Read a list from Trakt API. 39 | Export them into a CSV file.""" 40 | 41 | _trakt = { 42 | 'client_id' : '', # Auth details for trakt API 43 | 'client_secret' : '', # Auth details for trakt API 44 | 'oauth_token' : '', # Auth details for trakt API 45 | 'baseurl' : 'https://api.trakt.tv' # Sandbox environment https://api-staging.trakt.tv 46 | } 47 | 48 | _headers = { 49 | 'Accept' : 'application/json', # required per API 50 | 'Content-Type' : 'application/json', # required per API 51 | 'User-Agent' : 'Tratk importer', # User-agent 52 | 'Connection' : 'Keep-Alive', # Thanks to urllib3, keep-alive is 100% automatic within a session! 53 | 'trakt-api-version' : '2', # required per API 54 | 'trakt-api-key' : '', # required per API 55 | 'Authorization' : '', # required per API 56 | } 57 | 58 | _proxy = { 59 | 'proxy' : False, # True or False, trigger proxy use 60 | 'host' : 'https://127.0.0.1', # Host/IP of the proxy 61 | 'port' : '3128' # Port of the proxy 62 | } 63 | 64 | _proxyDict = { 65 | "http" : _proxy['host']+':'+_proxy['port'], 66 | "https" : _proxy['host']+':'+_proxy['port'] 67 | } 68 | 69 | response_arr = [] 70 | 71 | def read_config(options): 72 | """ 73 | Read config file and if provided overwrite default values 74 | If no config file exist, create one with default values 75 | """ 76 | global work_dir 77 | work_dir = '' 78 | if getattr(sys, 'frozen', False): 79 | work_dir = os.path.dirname(sys.executable) 80 | elif __file__: 81 | work_dir = os.path.dirname(__file__) 82 | _configfile = os.path.join(work_dir, options.config) 83 | if os.path.exists(options.config): 84 | _configfile = options.config 85 | if options.verbose: 86 | print "Config file: {0}".format(_configfile) 87 | if os.path.exists(_configfile): 88 | try: 89 | config = ConfigParser.SafeConfigParser() 90 | config.read(_configfile) 91 | if config.has_option('TRAKT','CLIENT_ID') and len(config.get('TRAKT','CLIENT_ID')) != 0: 92 | _trakt['client_id'] = config.get('TRAKT','CLIENT_ID') 93 | else: 94 | print 'Error, you must specify a trakt.tv CLIENT_ID' 95 | sys.exit(1) 96 | if config.has_option('TRAKT','CLIENT_SECRET') and len(config.get('TRAKT','CLIENT_SECRET')) != 0: 97 | _trakt['client_secret'] = config.get('TRAKT','CLIENT_SECRET') 98 | else: 99 | print 'Error, you must specify a trakt.tv CLIENT_SECRET' 100 | sys.exit(1) 101 | if config.has_option('TRAKT','OAUTH_TOKEN') and len(config.get('TRAKT','OAUTH_TOKEN')) != 0: 102 | _trakt['oauth_token'] = config.get('TRAKT','OAUTH_TOKEN') 103 | else: 104 | print 'Warning, authentification is required' 105 | if config.has_option('TRAKT','BASEURL'): 106 | _trakt['baseurl'] = config.get('TRAKT','BASEURL') 107 | if config.has_option('SETTINGS','PROXY'): 108 | _proxy['proxy'] = config.getboolean('SETTINGS','PROXY') 109 | if _proxy['proxy'] and config.has_option('SETTINGS','PROXY_HOST') and config.has_option('SETTINGS','PROXY_PORT'): 110 | _proxy['host'] = config.get('SETTINGS','PROXY_HOST') 111 | _proxy['port'] = config.get('SETTINGS','PROXY_PORT') 112 | _proxyDict['http'] = _proxy['host']+':'+_proxy['port'] 113 | _proxyDict['https'] = _proxy['host']+':'+_proxy['port'] 114 | except: 115 | print "Error reading configuration file {0}".format(_configfile) 116 | sys.exit(1) 117 | else: 118 | try: 119 | print '%s file was not found!' % _configfile 120 | config = ConfigParser.RawConfigParser() 121 | config.add_section('TRAKT') 122 | config.set('TRAKT', 'CLIENT_ID', '') 123 | config.set('TRAKT', 'CLIENT_SECRET', '') 124 | config.set('TRAKT', 'OAUTH_TOKEN', '') 125 | config.set('TRAKT', 'BASEURL', 'https://api.trakt.tv') 126 | config.add_section('SETTINGS') 127 | config.set('SETTINGS', 'PROXY', False) 128 | config.set('SETTINGS', 'PROXY_HOST', 'https://127.0.0.1') 129 | config.set('SETTINGS', 'PROXY_PORT', '3128') 130 | with open(_configfile, 'wb') as configfile: 131 | config.write(configfile) 132 | print "Default settings wrote to file {0}".format(_configfile) 133 | except: 134 | print "Error writing configuration file {0}".format(_configfile) 135 | sys.exit(1) 136 | 137 | def write_csv(options, results): 138 | """Write list output into a CSV file format""" 139 | if options.verbose: 140 | print "CSV output file: {0}".format(options.output) 141 | # Write result CSV 142 | with open(options.output, 'wb') as fp: 143 | keys = {} 144 | for i in results: 145 | for k in i.keys(): 146 | keys[k] = 1 147 | mycsv = csv.DictWriter(fp, fieldnames=keys.keys(), quoting=csv.QUOTE_ALL) 148 | mycsv.writeheader() 149 | for row in results: 150 | mycsv.writerow(row) 151 | fp.close() 152 | 153 | def api_auth(options): 154 | """API call for authentification OAUTH""" 155 | print("Open the link in a browser and paste the pincode when prompted") 156 | print("https://trakt.tv/oauth/authorize?response_type=code&" 157 | "client_id={0}&redirect_uri=urn:ietf:wg:oauth:2.0:oob".format( 158 | _trakt["client_id"])) 159 | pincode = str(raw_input('Input PIN:')) 160 | url = _trakt['baseurl'] + '/oauth/token' 161 | values = { 162 | "code": pincode, 163 | "client_id": _trakt["client_id"], 164 | "client_secret": _trakt["client_secret"], 165 | "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", 166 | "grant_type": "authorization_code" 167 | } 168 | 169 | request = requests.post(url, data=values) 170 | response = request.json() 171 | _headers['Authorization'] = 'Bearer ' + response["access_token"] 172 | _headers['trakt-api-key'] = _trakt['client_id'] 173 | print 'Save as "oauth_token" in file {0}: {1}'.format(options.config, response["access_token"]) 174 | 175 | def api_get_list(options, page): 176 | """API call for Sync / Get list by type""" 177 | url = _trakt['baseurl'] + '/sync/{list}/{type}?page={page}&limit={limit}'.format( 178 | list=options.list, type=options.type, page=page, limit=1000) 179 | if options.verbose: 180 | print(url) 181 | if _proxy['proxy']: 182 | r = requests.get(url, headers=_headers, proxies=_proxyDict, timeout=(10, 60)) 183 | else: 184 | r = requests.get(url, headers=_headers, timeout=(5, 60)) 185 | #pp.pprint(r.headers) 186 | if r.status_code != 200: 187 | print "Error fetching Get {list}: {status} [{text}]".format( 188 | list=options.list, status=r.status_code, text=r.text) 189 | return None 190 | else: 191 | global response_arr 192 | response_arr += json.loads(r.text) 193 | if 'X-Pagination-Page-Count'in r.headers and r.headers['X-Pagination-Page-Count']: 194 | print "Fetched page {page} of {PageCount} pages for {list} list".format( 195 | page=page, PageCount=r.headers['X-Pagination-Page-Count'], list=options.list) 196 | if page != int(r.headers['X-Pagination-Page-Count']): 197 | api_get_list(options, page+1) 198 | 199 | return response_arr 200 | 201 | def api_get_userlists(options, page): 202 | """API call for Sync / Get list by type""" 203 | url = _trakt['baseurl'] + '/users/{user}/lists'.format( 204 | user=options.userlist, page=page, limit=1000) 205 | #url = _trakt['baseurl'] + '/users/{user}/lists/{list_id}?page={page}&limit={limit}'.format( 206 | # list=options.list, type=options.type, page=page, limit=1000) 207 | if options.verbose: 208 | print(url) 209 | if _proxy['proxy']: 210 | r = requests.get(url, headers=_headers, proxies=_proxyDict, timeout=(10, 60)) 211 | else: 212 | r = requests.get(url, headers=_headers, timeout=(5, 60)) 213 | #pp.pprint(r.headers) 214 | if r.status_code != 200: 215 | print "Error fetching Get {list}: {status} [{text}]".format( 216 | list=options.list, status=r.status_code, text=r.text) 217 | return None 218 | else: 219 | global response_arr 220 | response_arr += json.loads(r.text) 221 | if 'X-Pagination-Page-Count'in r.headers and r.headers['X-Pagination-Page-Count']: 222 | print "Fetched page {page} of {PageCount} pages for {list} list".format( 223 | page=page, PageCount=r.headers['X-Pagination-Page-Count'], list=options.list) 224 | if page != int(r.headers['X-Pagination-Page-Count']): 225 | api_get_list(options, page+1) 226 | 227 | return response_arr 228 | 229 | def api_get_userlist(options, page): 230 | """API call for Sync / Get list by type""" 231 | url = _trakt['baseurl'] + '/users/{user}/lists/{list_id}/items/{type}?page={page}&limit={limit}'.format( 232 | user=options.userlist, list_id=options.listid, type=options.type, page=page, limit=1000) 233 | if options.verbose: 234 | print(url) 235 | if _proxy['proxy']: 236 | r = requests.get(url, headers=_headers, proxies=_proxyDict, timeout=(10, 60)) 237 | else: 238 | r = requests.get(url, headers=_headers, timeout=(5, 60)) 239 | #pp.pprint(r.headers) 240 | if r.status_code != 200: 241 | print "Error fetching Get {list}: {status} [{text}]".format( 242 | list=options.list, status=r.status_code, text=r.text) 243 | return None 244 | else: 245 | global response_arr 246 | response_arr += json.loads(r.text) 247 | if 'X-Pagination-Page-Count'in r.headers and r.headers['X-Pagination-Page-Count']: 248 | print "Fetched page {page} of {PageCount} pages for {list} list".format( 249 | page=page, PageCount=r.headers['X-Pagination-Page-Count'], list=options.list) 250 | if page != int(r.headers['X-Pagination-Page-Count']): 251 | api_get_userlist(options, page+1) 252 | 253 | return response_arr 254 | 255 | def api_remove_from_list(options, remove_data, is_id=False): 256 | """API call for Sync / Remove from list""" 257 | url = _trakt['baseurl'] + '/sync/{list}/remove'.format(list=options.list) 258 | if options.type == 'episodes': 259 | values = { 'shows' : remove_data } 260 | elif not is_id: 261 | values = { options.type : remove_data } 262 | else: 263 | values = { 'ids' : remove_data } 264 | json_data = json.dumps(values) 265 | if options.verbose: 266 | print(url) 267 | pp.pprint(json_data) 268 | if _proxy['proxy']: 269 | r = requests.post(url, data=json_data, headers=_headers, proxies=_proxyDict, timeout=(10, 60)) 270 | else: 271 | r = requests.post(url, data=json_data, headers=_headers, timeout=(5, 60)) 272 | if r.status_code != 200: 273 | print "Error removing items from {list}: {status} [{text}]".format( 274 | list=options.list, status=r.status_code, text=r.text) 275 | return None 276 | else: 277 | return json.loads(r.text) 278 | 279 | def main(): 280 | """ 281 | Main program loop 282 | * Read configuration file and validate 283 | * Authenticate if require 284 | * Export data from Trakt.tv 285 | * Cleanup list from Trakt.tv 286 | * Write to CSV 287 | """ 288 | ## Parse inputs if any 289 | parser = argparse.ArgumentParser(version='%(prog)s 0.3', description=desc, epilog=epilog) 290 | parser.add_argument('-c', '--config', 291 | help='allow to overwrite default config filename, default %(default)s', 292 | action='store', type=str, dest='config', default='tconfig.ini') 293 | parser.add_argument('-o', '--output', 294 | help='allow to overwrite default output filename, default %(default)s', 295 | nargs='?', type=str, const='export.csv', default=None) 296 | parser.add_argument('-t', '--type', 297 | help='allow to overwrite type, default %(default)s', 298 | choices=['movies', 'shows', 'episodes'], dest='type', default='movies') 299 | parser.add_argument('-l', '--list', 300 | help='allow to overwrite default list, default %(default)s', 301 | choices=['watchlist', 'collection', 'history'], dest='list', default='history') 302 | parser.add_argument('-u', '--userlist', 303 | help='allow to export a user custom list, default %(default)s', 304 | dest='userlist', default=None) 305 | parser.add_argument('-C', '--clean', 306 | help='empty list after export, default %(default)s', 307 | default=False, action='store_true', dest='clean') 308 | parser.add_argument('-D', '--duplicate', 309 | help='remove duplicate from list after export, default %(default)s', 310 | default=False, action='store_true', dest='dup') 311 | #parser.add_argument('-d', '--dryrun', 312 | # help='do not update the account, default %(default)s', 313 | # default=True, action='store_true', dest='dryrun') 314 | parser.add_argument('-V', '--verbose', 315 | help='print additional verbose information, default %(default)s', 316 | default=True, action='store_true', dest='verbose') 317 | options = parser.parse_args() 318 | 319 | ## Display debug information 320 | if options.verbose: 321 | print "Options: %s" % options 322 | 323 | if options.type == 'episodes' and options.list == "collection": 324 | print "Error, you can only fetch {0} from the history or watchlist list".format(options.type) 325 | sys.exit(1) 326 | 327 | if options.userlist: 328 | options.list = options.userlist 329 | 330 | if not options.output: 331 | options.output = 'export_{type}_{list}.csv'.format(type=options.type, list=options.list) 332 | 333 | ## Read configuration and validate 334 | read_config(options) 335 | 336 | ## Display oauth token if exist, otherwise authenticate to get one 337 | if _trakt['oauth_token']: 338 | _headers['Authorization'] = 'Bearer ' + _trakt['oauth_token'] 339 | _headers['trakt-api-key'] = _trakt['client_id'] 340 | else: 341 | api_auth(options) 342 | 343 | ## Display debug information 344 | if options.verbose: 345 | print "trakt: {}".format(_trakt) 346 | print "Authorization header: {}".format(_headers['Authorization']) 347 | 348 | ## Get lits from Trakt user 349 | export_data = [] 350 | if options.userlist: 351 | export_data = api_get_userlists(options, 1) 352 | if export_data: 353 | print "Found {0} user list".format(len(export_data)) 354 | #pp.pprint(export_data) 355 | for data in export_data: 356 | print "Found list id '{id}' name '{name}' with {items} items own by {own}".format( 357 | name=data['name'], id=data['ids']['trakt'], items=data['item_count'], own=data['user']['username']) 358 | print("Input the custom list id to export") 359 | options.listid = str(raw_input('Input:')) 360 | global response_arr ## Cleanup global.... 361 | response_arr = [] 362 | export_data = api_get_userlist(options, 1) 363 | #pp.pprint(export_data) 364 | if export_data: 365 | print "Found {0} Item-Count".format(len(export_data)) 366 | else: 367 | print "Error, no item return for {type} from the user list {list}".format( 368 | type=options.type, list=options.userlist) 369 | sys.exit(1) 370 | 371 | ## Get data from Trakt 372 | if not export_data: 373 | export_data = api_get_list(options, 1) 374 | if export_data: 375 | print "Found {0} Item-Count".format(len(export_data)) 376 | else: 377 | print "Error, no item return for {type} from the {list} list".format( 378 | type=options.type, list=options.list) 379 | sys.exit(1) 380 | 381 | if options.list == 'history': 382 | options.time = 'watched_at' 383 | elif options.list == 'watchlist': 384 | options.time = 'listed_at' 385 | elif options.list == 'collection': 386 | options.time = 'collected_at' 387 | elif option.userlist != None: 388 | options.time = 'listed_at' 389 | 390 | export_csv = [] 391 | find_dupids = [] 392 | for data in export_data: 393 | #pp.pprint(data) 394 | if options.type[:-1] != "episode" and 'imdb' in data[options.type[:-1]]['ids']: 395 | find_dupids.append(data[options.type[:-1]]['ids']['imdb']) 396 | export_csv.append({ 'imdb' : data[options.type[:-1]]['ids']['imdb'], 397 | 'trakt_id' : data[options.type[:-1]]['ids']['trakt'], 398 | options.time : data[options.time], 399 | 'title' : data[options.type[:-1]]['title'].encode('utf-8')}) 400 | elif 'tmdb' in data[options.type[:-1]]['ids']: 401 | find_dupids.append(data[options.type[:-1]]['ids']['tmdb']) 402 | if not data['episode']['title']: data['episode']['title'] = "no episode title" 403 | export_csv.append({ 'tmdb' : data[options.type[:-1]]['ids']['tmdb'], 404 | 'trakt_id' : data[options.type[:-1]]['ids']['trakt'], 405 | options.time : data[options.time], 406 | 'season' : data[options.type[:-1]]['season'], 407 | 'episode' : data[options.type[:-1]]['number'], 408 | 'episode_title' : data['episode']['title'].encode('utf-8'), 409 | 'show_title' : data['show']['title'].encode('utf-8')}) 410 | #print export_csv 411 | ## Write export data into CSV file 412 | write_csv(options, export_csv) 413 | 414 | ## Empty list after export 415 | if options.clean: 416 | cleanup_results = {'sentids' : 0, 'deleted' : 0, 'not_found' : 0} 417 | to_remove = [] 418 | for data in export_data: 419 | # TODO add filter 420 | #if data[options.time] == "2012-01-01T00:00:00.000Z": 421 | to_remove.append({'ids': data[options.type[:-1]]['ids']}) 422 | if len(to_remove) >= 10: # Remove by batch of 10 423 | cleanup_results['sentids'] += len(to_remove) 424 | result = api_remove_from_list(options, to_remove) 425 | if result: 426 | print "Result: {0}".format(result) 427 | if 'deleted' in result and result['deleted']: 428 | cleanup_results['deleted'] += result['deleted'][options.type] 429 | if 'not_found' in result and result['not_found']: 430 | cleanup_results['not_found'] += len(result['not_found'][options.type]) 431 | to_remove = [] 432 | # Remove the rest 433 | if len(to_remove) > 0: 434 | #print pp.pprint(data) 435 | cleanup_results['sentids'] += len(to_remove) 436 | result = api_remove_from_list(options, to_remove) 437 | if result: 438 | print "Result: {0}".format(result) 439 | if 'deleted' in result and result['deleted']: 440 | cleanup_results['deleted'] += result['deleted'][options.type] 441 | if 'not_found' in result and result['not_found']: 442 | cleanup_results['not_found'] += len(result['not_found'][options.type]) 443 | print "Overall cleanup {sent} {type}, results deleted:{deleted}, not_found:{not_found}".format( 444 | sent=cleanup_results['sentids'], type=options.type, 445 | deleted=cleanup_results['deleted'], not_found=cleanup_results['not_found']) 446 | 447 | ## Found duplicate and remove duplicate 448 | dup_ids = [item for item, count in collections.Counter(find_dupids).items() if count > 1] 449 | print "Found {dups} duplicate out of {total} {entry}".format( 450 | entry=options.type, dups=len(dup_ids), total=len(find_dupids)) 451 | if options.dup: 452 | if len(dup_ids) > 0: 453 | print dup_ids 454 | dup_results = {'sentids' : 0, 'deleted' : 0, 'not_found' : 0} 455 | to_remove = [] 456 | for dupid in find_dupids: 457 | count = 0 458 | for data in export_data: 459 | if data[options.type[:-1]]['ids']['imdb'] == dupid: 460 | #print "{0} {1}".format(dupid, data['id']) 461 | count += 1 462 | if count > 1: 463 | print "Removing {0} {1}".format(dupid, data['id']) 464 | to_remove.append(data['id']) 465 | dup_results['sentids'] += len(to_remove) 466 | result = api_remove_from_list(options, to_remove, is_id=True) 467 | if len(to_remove) >= 10: # Remove by batch of 10 468 | if result: 469 | print "Result: {0}".format(result) 470 | if 'deleted' in result and result['deleted']: 471 | dup_results['deleted'] += result['deleted'][options.type] 472 | if 'not_found' in result and result['not_found']: 473 | dup_results['not_found'] += len(result['not_found'][options.type]) 474 | to_remove = [] 475 | ## Remove the rest 476 | if len(to_remove) > 0: 477 | dup_results['sentids'] += len(to_remove) 478 | result = api_remove_from_list(options, to_remove, is_id=True) 479 | if result: 480 | print "Result: {0}".format(result) 481 | if 'deleted' in result and result['deleted']: 482 | dup_results['deleted'] += result['deleted'][options.type] 483 | if 'not_found' in result and result['not_found']: 484 | dup_results['not_found'] += len(result['not_found'][options.type]) 485 | to_remove = [] 486 | print "Overall {dup} duplicate {sent} {type}, results deleted:{deleted}, not_found:{not_found}".format( 487 | dup=len(dup_ids), sent=dup_results['sentids'], type=options.type, 488 | deleted=dup_results['deleted'], not_found=dup_results['not_found']) 489 | 490 | if __name__ == '__main__': 491 | main() 492 | -------------------------------------------------------------------------------- /get_imdbid.py: -------------------------------------------------------------------------------- 1 | import os, time, requests, json, sys, re, configparser 2 | 3 | config = configparser.ConfigParser() 4 | config.read('./config.ini') 5 | omdbapi_key = config['sonarr']['omdbapi_key'] 6 | add_count = 0 7 | failed_count = 0 8 | res = 0 9 | 10 | def main(): 11 | global add_count 12 | global failed_count 13 | global res 14 | if len(sys.argv)<2: print ("No list Specified... Bye!!"); sys.exit(-1) 15 | f=open(sys.argv[1], "r") 16 | if f.mode == 'r': f1 = f.readlines() 17 | print('\033c') 18 | res = input("This list contains S) TV Shows or M) Movies [S/M]? : ") 19 | if res == "S" or "s": res = 'series' 20 | elif res == "M" or "m": res = 'movie' 21 | else: print("Invalid Entry"); sys.exit(0) 22 | fo = open("{}.csv".format(res), "a") 23 | fo.write("title,year,imdbid\n") 24 | for x in f1: 25 | r = requests.get("https://www.omdbapi.com/?t={}&type={}&apikey={}".format(x.rstrip(),res,omdbapi_key)) 26 | if r.status_code == 200: 27 | item = json.loads(r.text) 28 | if item.get('Response') == "False": 29 | failed_count +=1 30 | ff = open("{}_failed".format(res), "a") 31 | ff.write("{} Failed to be Matched\n".format(x.rstrip())) 32 | ff.close() 33 | continue 34 | else: 35 | year = item.get('Year') 36 | imdbid = item.get('imdbID') 37 | title = item.get('Title') 38 | title = re.sub('[(]\d{4}[)]','',title) 39 | fo.write("{},{},{}\n".format(title.rstrip(),year[:4],imdbid)) 40 | #print ("{},{},{}".format(title.rstrip(),year[:4],imdbid)) 41 | print('\033c') 42 | print("Matching {} {}.".format(title,year[:4])) 43 | add_count +=1 44 | print("\u001b[32mMatched {} of {}, {} Failed to match. {} file was created.\u001b[0m".format(add_count,len(f1),failed_count,res)) 45 | fo.close() 46 | 47 | if __name__ == "__main__": 48 | main() 49 | 50 | -------------------------------------------------------------------------------- /lidarr_add_from_list.py: -------------------------------------------------------------------------------- 1 | import os, time, requests, logging, logging.handlers, json, sys, re, csv 2 | from colorlog import ColoredFormatter 3 | import configparser 4 | from datetime import datetime 5 | 6 | artist_added_count=0 7 | artist_exist_count=0 8 | 9 | # Config ############################################################################################################### 10 | 11 | config = configparser.ConfigParser() 12 | config.read('./config.ini') 13 | baseurl = config['lidarr']['baseurl'] 14 | # urlbase = config['lidarr']['urlbase'] 15 | api_key = config['lidarr']['api_key'] 16 | rootfolderpath = config['lidarr']['rootfolderpath'] 17 | 18 | # Logging ############################################################################################################## 19 | 20 | logging.getLogger().setLevel(logging.NOTSET) 21 | 22 | formatter = ColoredFormatter( 23 | "%(log_color)s[%(levelname)s]%(reset)s %(white)s%(message)s", 24 | datefmt=None, 25 | reset=True, 26 | log_colors={ 27 | 'DEBUG': 'cyan', 28 | 'INFO': 'green', 29 | 'WARNING': 'yellow', 30 | 'ERROR': 'red', 31 | 'CRITICAL': 'red,bg_white', 32 | }, 33 | secondary_log_colors={}, 34 | style='%' 35 | ) 36 | 37 | logger = logging.StreamHandler() 38 | logger.setLevel(logging.INFO) # DEBUG To show all 39 | logger.setFormatter(formatter) 40 | logging.getLogger().addHandler(logger) 41 | if not os.path.exists("./logs/"): os.mkdir("./logs/") 42 | logFileName = "./logs/lafl.log"#.format(datetime.now().strftime("%Y-%m-%d-%H.%M.%S")) 43 | filelogger = logging.handlers.RotatingFileHandler(filename=logFileName) 44 | filelogger.setLevel(logging.DEBUG) 45 | logFormatter = logging.Formatter("%(asctime)s [%(levelname)-5.5s] %(message)s") 46 | filelogger.setFormatter(logFormatter) 47 | logging.getLogger().addHandler(filelogger) 48 | 49 | log = logging.getLogger("app." + __name__) 50 | 51 | ######################################################################################################################## 52 | 53 | def add_artist(artistName,foreignArtistId): 54 | global artist_exist_count, artist_added_count 55 | artistIds = [] 56 | for artist_to_add in LidarrData: artistIds.append(artist_to_add.get('id')) 57 | if foreignArtistId not in artistIds: 58 | data = json.dumps({ 59 | "artistName" : artistName , 60 | "foreignArtistId" : foreignArtistId, 61 | "QualityProfileId" : 1, 62 | "MetadataProfileId" : 1, 63 | "Path": os.path.join(rootfolderpath,artistName) , 64 | "albumFolder": True , 65 | "RootFolderPath" : rootfolderpath, 66 | "monitored": True, 67 | "addOptions": {"searchForMissingAlbums" : False} 68 | }) 69 | url = '{}/api/v1/artist'.format(baseurl) 70 | headers = {"Content-type": "application/json", "X-Api-Key": "{}".format(api_key)} 71 | rsp = requests.post(url, headers=headers, data=data) 72 | if rsp.status_code == 201: 73 | artist_added_count +=1 74 | log.info("{} Added to Lidarr :)".format(artistName)) 75 | elif rsp.status_code == 400: 76 | artist_exist_count +=1 77 | log.info("{} already Exists in Lidarr.".format(artistName)) 78 | return 79 | else: 80 | log.error("{} Not found, Not added to Lidarr.".format(artistName)) 81 | log.error("URL -> {} Status Code -> {}".format(url,rsp.status_code)) 82 | return 83 | else: 84 | artist_exist_count +=1 85 | log.info("{} already Exists in Lidarr.".format(artistName)) 86 | return 87 | 88 | def get_artist_id(artist): 89 | url = 'https://api.lidarr.audio/api/v0.4/search?type=artist&query="{}"'.format(artist) 90 | headers = {"Content-type": "application/json", "X-Api-Key": "{}".format(api_key)} 91 | rsp = requests.get(url, headers=headers) 92 | 93 | if rsp.text =="[]": 94 | log.error("Sorry. We couldn't find {}".format(artist)) 95 | fo = open("not_found.txt", "a+") 96 | fo.write("{}\n".format(artist)) 97 | fo.close 98 | return None 99 | 100 | d = json.loads(rsp.text) 101 | if rsp.status_code == 200: 102 | try: return d[0]['id'] 103 | except Exception as e: return d['id'] 104 | else: 105 | return None 106 | 107 | def main(): 108 | # print('\033c') 109 | global artist_exist_count 110 | global LidarrData 111 | 112 | if sys.version_info[0] < 3: log.error("Must be using Python 3"); sys.exit(-1) 113 | if len(sys.argv)<2: log.error("No list Specified... Bye!!"); sys.exit(-1) 114 | if not os.path.exists(sys.argv[1]): log.info("{} Does Not Exist".format(sys.argv[1]));sys.exit(-1) 115 | log.info("Downloading Lidarr Artist Data. :)") 116 | headers = {"Content-type": "application/json", "X-Api-Key": api_key } 117 | url = "{}/api/v1/artist".format(baseurl) 118 | rsp = requests.get(url , headers=headers) 119 | if rsp.status_code == 200: 120 | LidarrData = json.loads(rsp.text) 121 | elif rsp.status_code == 401: 122 | log.error("Failed to connect to Lidarr Unauthorized. Check api_key in config."); sys.exit(-1) 123 | else: 124 | log.error("URL -> {} Status Code -> {}".format(url,rsp.status_code)) 125 | log.error("Failed to connect to Lidarr..."); sys.exit(-1) 126 | 127 | with open(sys.argv[1], encoding="utf8") as csvfile: total_count = len(list(csv.DictReader(csvfile))) 128 | with open(sys.argv[1]) as csvfile: 129 | m = csv.DictReader(csvfile) 130 | if not total_count>0: log.error("No Artists Found in file... Bye!!"); sys.exit() 131 | log.info("Found {} Artists in {}. :)".format(total_count,sys.argv[1])) 132 | for row in m: 133 | if not (row): continue 134 | try: row['artist'] 135 | except: log.error("Invalid CSV File, Header does not contain artist header."); sys.exit(-1) 136 | artist = row['artist']; foreignArtistId = row['foreignArtistId'] 137 | if foreignArtistId == None: foreignArtistId = get_artist_id(artist) 138 | if foreignArtistId == None: continue 139 | try: add_artist(artist, foreignArtistId) 140 | except Exception as e: log.error(e); sys.exit(-1) 141 | log.info("Added {} of {} Artists, {} Already Exist".format(artist_added_count,total_count,artist_exist_count)) 142 | 143 | if __name__ == "__main__": 144 | main() -------------------------------------------------------------------------------- /movies.csv: -------------------------------------------------------------------------------- 1 | title,year,imdbid 2 | The Do-Over,,tt4769836 3 | The Ridiculous 6,,tt2479478 4 | Pixels,,tt2120120 5 | Hotel Transylvania 2,,tt2510894 6 | The Cobbler,,tt3203616 7 | Blended,,tt1086772 8 | Grown Ups 2,,tt2191701 9 | That's My Boy,,tt1232200 10 | Jack and Jill,,tt0810913 11 | Just Go with It,,tt1564367 12 | Grown Ups,,tt1375670 13 | Funny People,,tt1201167 14 | You Don't Mess with the Zohan,,tt0960144 15 | Bedtime Stories,,tt0960731 16 | I Now Pronounce You Chuck & Larry,,tt0762107 17 | Reign Over Me,,tt0490204 18 | Click,,tt0389860 19 | Deuce Bigalow: European Gigolo,,tt0367652 20 | The Longest Yard,,tt0398165 21 | Spanglish,,tt0371246 22 | 50 First Dates,,tt0343660 23 | Anger Management,,tt0305224 24 | Pauly Shore Is Dead,,tt0284674 25 | Eight Crazy Nights,,tt0271263 26 | The Hot Chick,,tt0302640 27 | Punch-Drunk Love,,tt0272338 28 | Mr. Deeds,,tt0280590 29 | The Animal,,tt0255798 30 | Little Nicky,,tt0185431 31 | Deuce Bigalow: Male Gigolo,,tt0205000 32 | Big Daddy,,tt0142342 33 | The Wedding Singer,,tt0120888 34 | The Waterboy,,tt0120484 35 | Bulletproof,,tt0115783 36 | Happy Gilmore,,tt0116483 37 | Billy Madison,,tt0112508 38 | Mixed Nuts,,tt0110538 39 | Airheads,,tt0109068 40 | Coneheads,,tt0106598 41 | Happy Death Day 2U,,tt8155288 42 | Vice,,tt6266538 43 | The Mule,,tt7959026 44 | Can You Ever Forgive Me?,,tt4595882 45 | Cold Pursuit,,tt5719748 46 | Night School,,tt6781982 47 | Avengers: Infinity War,,tt4154756 48 | Close,,tt5316540 49 | Mission: Impossible - Fallout,,tt4912910 50 | The Martian,,tt3659388 51 | Passengers,,tt1355644 -------------------------------------------------------------------------------- /radarr_add_from_list.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import csv 3 | import json 4 | import logging.handlers 5 | import os 6 | import requests 7 | import sys 8 | import urllib.parse 9 | 10 | from colorlog import ColoredFormatter 11 | 12 | movie_added_count = 0 13 | movie_exist_count = 0 14 | 15 | 16 | 17 | # Config ############################################################################################################### 18 | 19 | config = configparser.ConfigParser() 20 | config.read('./config.ini') 21 | baseurl = config['radarr']['baseurl'] 22 | urlbase = config['radarr']['urlbase'] 23 | api_key = config['radarr']['api_key'] 24 | rootfolderpath = config['radarr']['rootfolderpath'] 25 | searchForMovie = config['radarr']['searchForMovie'] 26 | if searchForMovie == "1" or searchForMovie == "True": 27 | searchForMovie = True 28 | else: 29 | searchForMovie = False 30 | quality_profile_id = config['radarr']['qualityProfileId'] 31 | omdbapi_key = config['radarr']['omdbapi_key'] 32 | 33 | # Logging ############################################################################################################## 34 | 35 | logging.getLogger().setLevel(logging.NOTSET) 36 | 37 | formatter = ColoredFormatter( 38 | "%(log_color)s[%(levelname)s]%(reset)s %(white)s%(message)s", 39 | datefmt=None, 40 | reset=True, 41 | log_colors={ 42 | 'DEBUG': 'cyan', 43 | 'INFO': 'green', 44 | 'WARNING': 'yellow', 45 | 'ERROR': 'red', 46 | 'CRITICAL': 'red,bg_white', 47 | }, 48 | secondary_log_colors={}, 49 | style='%' 50 | ) 51 | 52 | logger = logging.StreamHandler() 53 | logger.setLevel(logging.INFO) # DEBUG To show all 54 | logger.setFormatter(formatter) 55 | logging.getLogger().addHandler(logger) 56 | if not os.path.exists("./logs/"): os.mkdir("./logs/") 57 | logFileName = "./logs/rafl.log" # .format(datetime.now().strftime("%Y-%m-%d-%H.%M.%S")) 58 | filelogger = logging.handlers.RotatingFileHandler(filename=logFileName) 59 | filelogger.setLevel(logging.DEBUG) 60 | logFormatter = logging.Formatter("%(asctime)s [%(levelname)-5.5s] %(message)s") 61 | filelogger.setFormatter(logFormatter) 62 | logging.getLogger().addHandler(filelogger) 63 | 64 | log = logging.getLogger("app." + __name__) 65 | 66 | 67 | ######################################################################################################################## 68 | 69 | def add_movie(title, year, imdbid, quality_profile_id): 70 | 71 | global movie_added_count 72 | global movie_exist_count 73 | 74 | if year == "" or year is None: 75 | year = get_year(title) 76 | if imdbid == "" or imdbid is None: 77 | imdbid = get_imdbid(title, year) 78 | 79 | # Store Radarr Server imdbid for faster matching 80 | current_movie_ids = [movie.get('imdbId') for movie in RadarrData] 81 | 82 | if match_profile_id(quality_profile_id): ProfileId = get_profile_from_name(quality_profile_id) 83 | 84 | if imdbid is None: 85 | log.warning(f"Cannot find IMDbId for {title}, attempting to add without it...") 86 | # Search by Movie Title in Radarr 87 | headers = {"Content-type": "application/json", 'Accept': 'application/json'} 88 | url = f"{baseurl}{urlbase}/api/v3/movie/lookup?term={title.replace(' ', '%20')}&apikey={api_key}" 89 | radarr_api_response = requests.get(url, headers=headers) 90 | data = json.loads(radarr_api_response.text) 91 | if radarr_api_response.text == "[]": 92 | log.error(f"Sorry. We couldn't find any movies matching {title} ({year})") 93 | return 94 | if radarr_api_response.status_code == 200: 95 | tmdbid = data[0]["tmdbId"] 96 | title = data[0]["title"] 97 | year = data[0]['year'] 98 | images = json.loads(json.dumps(data[0]["images"])) 99 | titleslug = data[0]["titleSlug"] 100 | movie_data = json.dumps({ 101 | "title": title, 102 | "qualityProfileId": ProfileId, 103 | "year": year, 104 | "tmdbId": tmdbid, 105 | "titleslug": titleslug, 106 | "monitored": True, 107 | "minimumAvailability": "released", 108 | "rootFolderPath": rootfolderpath, 109 | "images": images, 110 | "addOptions": {"searchForMovie": searchForMovie} 111 | }) 112 | # Add Movie To Radarr 113 | headers = {"Content-type": "application/json", 'Accept': 'application/json', "X-Api-Key": api_key} 114 | url = f'{baseurl}{urlbase}/api/v3/movie' 115 | radarr_api_response = requests.post(url, headers=headers, data=movie_data) 116 | log.debug(f"Radarr response: {radarr_api_response.status_code}: {radarr_api_response.text}") 117 | if radarr_api_response.status_code == 201: 118 | movie_added_count += 1 119 | if searchForMovie: 120 | log.info(f"{tmdbid}\t {title} ({year}) Added to Radarr :) NowSearching.") 121 | else: 122 | log.info(f"{tmdbid}\t {title} ({year}) Added to Radarr :) Search Disabled.") 123 | elif radarr_api_response.status_code == 400: 124 | movie_exist_count += 1 125 | log.info(f"{tmdbid}\t {title} ({year}) already Exists in Radarr.") 126 | return 127 | else: 128 | log.error(f"{imdbid}\t {title} ({year}) Not found, Not added to Radarr.") 129 | return 130 | 131 | elif imdbid not in current_movie_ids: 132 | log.warning(f"Found IMDbId for {title} - attempting to add to Radarr...") 133 | # Build json Data to import into radarr 134 | session = requests.Session() 135 | adapter = requests.adapters.HTTPAdapter(max_retries=20) 136 | session.mount('https://', adapter) 137 | session.mount('http://', adapter) 138 | 139 | headers = {"Content-type": "application/json", 'Accept': 'application/json'} 140 | url = f"{baseurl}{urlbase}/api/v3/movie/lookup/imdb?imdbId={imdbid}&apikey={api_key}" 141 | radarr_api_response = session.get(url, headers=headers) 142 | if len(radarr_api_response.text) == 0: 143 | log.error(f"Sorry. We couldn't find any movies matching {title} ({year})") 144 | return 145 | if radarr_api_response.status_code == 200: 146 | data = json.loads(radarr_api_response.text) 147 | tmdbid = data["tmdbId"] 148 | title = data["title"] 149 | year = data['year'] 150 | images = json.loads(json.dumps(data["images"])) 151 | titleslug = data["titleSlug"] 152 | movie_data = json.dumps({ 153 | "title": title, 154 | "qualityProfileId": ProfileId, 155 | "year": year, 156 | "tmdbId": tmdbid, 157 | "titleslug": titleslug, 158 | "monitored": True, 159 | "minimumAvailability": "released", 160 | "rootFolderPath": rootfolderpath, 161 | "images": images, 162 | "addOptions": {"searchForMovie": searchForMovie}}) 163 | elif radarr_api_response.status_code == 404: 164 | log.error(f"{imdbid}\t {title} ({year}) Movie not found... unable to add to Radarr") 165 | return 166 | elif radarr_api_response.status_code == 500: 167 | log.error(f"{imdbid}\t {title} ({year}) Can't find TMDB ID - movie may have been removed!") 168 | return 169 | else: 170 | log.error("Something else has happened.") 171 | return 172 | # Add Movie To Radarr 173 | headers = {"Content-type": "application/json", 'Accept': 'application/json', "X-Api-Key": api_key} 174 | url = f'{baseurl}{urlbase}/api/v3/movie' 175 | radarr_api_response = requests.post(url, headers=headers, data=movie_data) 176 | log.debug(f"Radarr response: {radarr_api_response.status_code}: {radarr_api_response.text}") 177 | if radarr_api_response.status_code == 201: 178 | log.debug("Connected to Radarr!") 179 | movie_added_count += 1 180 | if searchForMovie: # Check If you want to force download search 181 | log.info( 182 | f"{imdbid}\t {title} ({year}) Added to Radarr :) Now Searching.") 183 | else: 184 | log.info( 185 | f"{imdbid}\t {title} ({year}) Added to Radarr :) Search Disabled.") 186 | else: 187 | movie_exist_count += 1 188 | log.info(f"{imdbid}\t {title} ({year}) already exists in Radarr!") 189 | return 190 | 191 | def get_profile_from_name(name :str) -> int: 192 | """ 193 | Converts Profile Name -> ID 194 | :return: ID 195 | :type: any 196 | """ 197 | try: 198 | number = int(name) 199 | return name 200 | except ValueError: 201 | profiles = quality_profiles 202 | profile = next((item for item in profiles if item["name"] == name), False) 203 | if not profile: 204 | log.error(f"Could not find profile_id for instance profile ID {name}") 205 | return select_profile_id() 206 | return profile.get('id') 207 | 208 | 209 | def get_quality_profiles() -> list: 210 | """ 211 | Parses local Radarr API to get the server's quality profiles and return them to calling 212 | functions 213 | :return: The server's quality profiles as a list 214 | :type: list 215 | """ 216 | log.info("Getting quality profiles...") 217 | headers = {"Content-type": "application/json", "X-Api-Key": f"{api_key}"} 218 | url = f"{baseurl}{urlbase}/api/v3/qualityProfile" 219 | r = requests.get(url, headers=headers) 220 | profiles_json = json.loads(r.text) 221 | return profiles_json 222 | 223 | 224 | def select_profile_id(): 225 | """ 226 | Allows the user to select a quality profile ID at runtime using console input 227 | :return: Returns an integer that matches the specified quality profile Id 228 | :type: int 229 | 230 | """ 231 | selected = False 232 | profile_choice = -1 233 | print("\nPlease enter a valid profile ID:") 234 | for profile in quality_profiles: 235 | print(f"{profile.get('id')}: {profile.get('name')}") 236 | while not selected: 237 | user_input = input("> ") 238 | selected = match_profile_id(user_input) 239 | if selected: 240 | profile_choice = user_input 241 | return profile_choice 242 | 243 | 244 | def match_profile_id(quality_id) -> bool: 245 | """ 246 | Checks if a given quality profile ID matches with one from the API. 247 | :type: bool 248 | :param quality_id: Quality profile ID as a string of int 249 | :return: Returns true if the quality profile is matched/found 250 | """ 251 | profiles = quality_profiles 252 | if not quality_id.isdigit(): 253 | profile = next((p for p in profiles if p["name"] == quality_id), False) 254 | else: 255 | profile = next((p for p in profiles if p['id'] == int(quality_id)), False) 256 | if not profile: 257 | log.error(f'Could not find profile_id for instance profile ID: {quality_id}') 258 | if match_profile_id(select_profile_id()): return True 259 | return True 260 | 261 | 262 | def get_imdbid(title: str, year: str) -> str | None: 263 | """ 264 | Uses OMDB to return the IMDB ID (ttXXXXXXX) of a movie 265 | :param title: Title of the movie 266 | :param year: Year the movie released 267 | :return: IMDB ID of the movie if found, or None if not. 268 | """ 269 | # Get Movie imdbid 270 | log.info(f"Getting IMDbId for {title}") 271 | parsed_movie_title = urllib.parse.quote(title, "UTF-8") 272 | headers = {"Content-type": "application/json", 'Accept': 'application/json'} 273 | r = requests.get(f"https://www.omdbapi.com/?t={parsed_movie_title}&y={year}&type=movie&apikey={omdbapi_key}", 274 | headers=headers) 275 | if r.status_code == 401: 276 | log.error("OMDb API Request limit reached!") 277 | d = json.loads(r.text) 278 | if r.status_code == 200: 279 | if d.get('Response') == "False": 280 | return None 281 | else: 282 | return d.get('imdbID') 283 | else: 284 | return None 285 | 286 | 287 | def get_year(imdbid: str) -> str | None: 288 | """ 289 | Uses OMDB to check the year that a given movie released 290 | :param imdbid: The IMDB ID of a movie 291 | :return: Year the movie released if found, None if not 292 | """ 293 | # Get Movie Year 294 | headers = {"Content-type": "application/json", 'Accept': 'application/json'} 295 | r = requests.get(f"https://www.omdbapi.com/?t={imdbid}&apikey={omdbapi_key}", headers=headers) 296 | if r.status_code == 401: 297 | log.error("OMDb API Request limit reached!") 298 | d = json.loads(r.text) 299 | if r.status_code == 200: 300 | if d.get('Response') == "False": 301 | return None 302 | else: 303 | return d.get('Year') 304 | else: 305 | return None 306 | 307 | 308 | def main(): 309 | global RadarrData 310 | global quality_profiles 311 | global quality_profile_id 312 | 313 | print('\033c') 314 | if sys.version_info[0] < 3: 315 | log.error("Must be using Python 3") 316 | sys.exit(-1) 317 | if len(sys.argv) < 2: 318 | log.error("No CSV file specified... bye!!") 319 | sys.exit(-1) 320 | if not os.path.exists(sys.argv[1]): 321 | log.error(f"{sys.argv[1]} does not exist!") 322 | sys.exit(-1) 323 | 324 | log.info("Downloading Radarr Movie Data. :)") 325 | headers = {"Content-type": "application/json", "X-Api-Key": api_key} 326 | url = f"{baseurl}{urlbase}/api/v3/movie" 327 | rsp = requests.get(url, headers=headers) 328 | if rsp.status_code == 200: 329 | RadarrData = json.loads(rsp.text) 330 | quality_profiles = get_quality_profiles() 331 | else: 332 | log.error("Failed to connect to Radarr...") 333 | 334 | log.info(f"Loading {sys.argv[1]}...") 335 | with open(sys.argv[1], encoding="ISO-8859-1", errors='ignore') as csvfile: 336 | total_count = len(list(csv.DictReader(csvfile))) 337 | with open(sys.argv[1], encoding="ISO-8859-1", errors='ignore') as csvfile: 338 | movies_list = csv.DictReader(csvfile) 339 | if not total_count > 0: 340 | log.error("No movies found in CSV file.") 341 | exit() 342 | if movies_list.fieldnames != ["title", "year", "imdbid"]: 343 | log.error("Invalid CSV file - header does not contain title,year,imdbid") 344 | sys.exit(-1) 345 | log.info(f"Found {total_count} movies in {sys.argv[1]} :)") 346 | for row in movies_list: 347 | title = row['title'] 348 | year = row['year'] 349 | imdbid = row['imdbid'] 350 | try: 351 | if quality_profile_id is None or quality_profile_id == '': 352 | log.warning("Quality profile not set in config file.") 353 | quality_profile_id = select_profile_id() 354 | add_movie(title, year, imdbid,quality_profile_id) 355 | except Exception as e: 356 | exc_type, exc_obj, exc_tb = sys.exc_info() 357 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 358 | log.error(exc_type, fname, exc_tb.tb_lineno) 359 | sys.exit(-1) 360 | log.info(f"Added {movie_added_count} of {total_count} movies - {movie_exist_count} already existed ;)") 361 | 362 | 363 | if __name__ == "__main__": 364 | main() 365 | -------------------------------------------------------------------------------- /radarr_remove_downloaded.py: -------------------------------------------------------------------------------- 1 | import os, requests, json, logging, logging.handlers, sys 2 | from colorlog import ColoredFormatter 3 | import configparser 4 | 5 | # Config ############################################################################################################### 6 | 7 | config = configparser.ConfigParser() 8 | config.read('./config.ini') 9 | baseurl = config['radarr']['baseurl'] 10 | api_key = config['radarr']['api_key'] 11 | 12 | 13 | # Logging ############################################################################################################## 14 | 15 | formatter = ColoredFormatter( 16 | "%(log_color)s[%(levelname)s]%(reset)s %(white)s%(message)s", 17 | datefmt=None, 18 | reset=True, 19 | log_colors={ 20 | 'DEBUG': 'cyan', 21 | 'INFO': 'green', 22 | 'WARNING': 'yellow', 23 | 'ERROR': 'red', 24 | 'CRITICAL': 'red,bg_white', 25 | }, 26 | secondary_log_colors={}, 27 | style='%' 28 | ) 29 | 30 | logging.getLogger().setLevel(logging.NOTSET) 31 | 32 | logger = logging.StreamHandler(sys.stdout) 33 | logger.setLevel(logging.INFO) # DEBUG To show all 34 | # logFormatter = logging.Formatter("\033[1;31;32m[%(levelname)s] \u001b[0m%(message)s") 35 | logger.setFormatter(formatter) 36 | logging.getLogger().addHandler(logger) 37 | 38 | 39 | rotatingHandler = logging.handlers.RotatingFileHandler(filename='./rrd.log') 40 | rotatingHandler.setLevel(logging.DEBUG) 41 | logFormatter = logging.Formatter("%(asctime)s [%(levelname)-5.5s] %(message)s") 42 | rotatingHandler.setFormatter(logFormatter) 43 | logging.getLogger().addHandler(rotatingHandler) 44 | 45 | log = logging.getLogger("app." + __name__) 46 | 47 | ######################################################################################################################## 48 | 49 | print('\033c') 50 | if sys.version_info[0] < 3: log.error("Must be using Python 3"); sys.exit(-1) 51 | log.info("Downloading Radarr Movie Data...") 52 | headers = {"Content-type": "application/json", "X-Api-Key": api_key } 53 | url = "{}/api/movie".format(baseurl) 54 | rsp = requests.get(url, headers=headers) 55 | data = json.loads(rsp.text) 56 | count = 0 57 | for i in data: 58 | headers = {"Content-type": "application/json", "X-Api-Key": api_key } 59 | url = "{}/api/movie/{}?deleteFiles=false&addExclusion=false".format(baseurl,i['id']) 60 | rsp = requests.delete(url, headers=headers) 61 | print(rsp.text) 62 | if rsp.status_code == 200: 63 | log.info ("\u001b[36m{} ({})\u001b[0m \u001b[31mRemoving from Radarr...\u001b[0m".format(i['title'],i['year'])) 64 | 65 | # if i['hasFile']: print(json.dumps(i, indent=4, sort_keys=True)) 66 | # if i['year'] < 1980 and i['hasFile']: 67 | # if os.path.exists(i['path']): 68 | # rsp = requests.delete(url, headers=headers) 69 | # if rsp.status_code == 200: 70 | # count +=1 71 | # log.info ("\u001b[36m{} ({})\u001b[0m is older than 1980, \u001b[31mRemoving from Radarr...\u001b[0m".format(i['title'],i['year'])) 72 | if i['hasFile']: 73 | if os.path.exists(i['path']): 74 | # if "720" or "1080" in i['movieFile']['quality']['quality']['name']: 75 | rsp = requests.delete(url, headers=headers) 76 | if rsp.status_code == 200: 77 | count +=1 78 | log.info ("\u001b[36m{} ({})\u001b[0m Has been downloaded, \u001b[31mRemoving from Radarr...\u001b[0m".format(i['title'],i['year'])) 79 | log.info ("Removed {} Movies.".format(count)) 80 | -------------------------------------------------------------------------------- /radarr_unmonitor_downloaded.py: -------------------------------------------------------------------------------- 1 | import os, requests, json, logging, logging.handlers, sys 2 | from colorlog import ColoredFormatter 3 | import configparser 4 | 5 | # Config ############################################################################################################### 6 | 7 | config = configparser.ConfigParser() 8 | config.read('./config.ini') 9 | baseurl = config['radarr']['baseurl'] 10 | urlbase = config['radarr']['urlbase'] 11 | api_key = config['radarr']['api_key'] 12 | if urlbase != "": baseurl = "{}{}".format(baseurl,urlbase) 13 | 14 | # Logging ############################################################################################################## 15 | 16 | formatter = ColoredFormatter( 17 | "%(log_color)s[%(levelname)s]%(reset)s %(white)s%(message)s", 18 | datefmt=None, 19 | reset=True, 20 | log_colors={ 21 | 'DEBUG': 'cyan', 22 | 'INFO': 'green', 23 | 'WARNING': 'yellow', 24 | 'ERROR': 'red', 25 | 'CRITICAL': 'red,bg_white', 26 | }, 27 | secondary_log_colors={}, 28 | style='%' 29 | ) 30 | 31 | logging.getLogger().setLevel(logging.NOTSET) 32 | 33 | logger = logging.StreamHandler(sys.stdout) 34 | logger.setLevel(logging.INFO) # DEBUG To show all 35 | # logFormatter = logging.Formatter("\033[1;31;32m[%(levelname)s] \u001b[0m%(message)s") 36 | logger.setFormatter(formatter) 37 | logging.getLogger().addHandler(logger) 38 | log = logging.getLogger("app." + __name__) 39 | 40 | ######################################################################################################################## 41 | 42 | print('\033c') 43 | if sys.version_info[0] < 3: log.error("Must be using Python 3"); sys.exit(-1) 44 | log.info("Downloading Radarr Movie Data...") 45 | headers = {"Content-type": "application/json", "X-Api-Key": api_key } 46 | url = "{}/api/movie".format(baseurl) 47 | rsp = requests.get(url, headers=headers) 48 | data = json.loads(rsp.text) 49 | count = 0 50 | for i in data: 51 | headers = {"Content-type": "application/json", "X-Api-Key": api_key } 52 | url = "{}/api/movie/{}".format(baseurl,i['id']) 53 | rsp = requests.get(url, headers=headers) 54 | if rsp.status_code == 200: 55 | if i['hasFile']: 56 | if os.path.exists(i['path']): 57 | if i['monitored'] == True: 58 | # print(json.dumps(i, indent=4, sort_keys=True)) 59 | data = json.loads(json.dumps(i)) 60 | data['monitored'] = False 61 | data = json.dumps(data, indent=4, sort_keys=True) 62 | headers = {"Content-type": "application/json", 'Accept':'application/json', "X-Api-Key": api_key} 63 | url = '{}/api/movie'.format(baseurl) 64 | rsp = requests.put(url, headers=headers, data=data) 65 | if rsp.status_code == 201: 66 | count += count 67 | log.info ("\u001b[36m{} ({})\u001b[0m Has been downloaded, \u001b[31mUnmonitoring....\u001b[0m".format(i['title'],i['year'])) 68 | log.info ("Unmonitored {} Movies.".format(count)) 69 | -------------------------------------------------------------------------------- /remove_from_sonarr.py: -------------------------------------------------------------------------------- 1 | import os, requests, json, logging, logging.handlers, sys 2 | from colorlog import ColoredFormatter 3 | import configparser 4 | 5 | # Config ############################################################################################################### 6 | 7 | config = configparser.ConfigParser() 8 | config.read('./config.ini') 9 | baseurl = config['sonarr']['baseurl'] 10 | api_key = config['sonarr']['api_key'] 11 | 12 | 13 | # Logging ############################################################################################################## 14 | 15 | formatter = ColoredFormatter( 16 | "%(log_color)s[%(levelname)s]%(reset)s %(white)s%(message)s", 17 | datefmt=None, 18 | reset=True, 19 | log_colors={ 20 | 'DEBUG': 'cyan', 21 | 'INFO': 'green', 22 | 'WARNING': 'yellow', 23 | 'ERROR': 'red', 24 | 'CRITICAL': 'red,bg_white', 25 | }, 26 | secondary_log_colors={}, 27 | style='%' 28 | ) 29 | 30 | logging.getLogger().setLevel(logging.NOTSET) 31 | 32 | logger = logging.StreamHandler(sys.stdout) 33 | logger.setLevel(logging.INFO) # DEBUG To show all 34 | # logFormatter = logging.Formatter("\033[1;31;32m[%(levelname)s] \u001b[0m%(message)s") 35 | logger.setFormatter(formatter) 36 | logging.getLogger().addHandler(logger) 37 | 38 | 39 | rotatingHandler = logging.handlers.RotatingFileHandler(filename='./rfs.log') 40 | rotatingHandler.setLevel(logging.DEBUG) 41 | logFormatter = logging.Formatter("%(asctime)s [%(levelname)-5.5s] %(message)s") 42 | rotatingHandler.setFormatter(logFormatter) 43 | logging.getLogger().addHandler(rotatingHandler) 44 | 45 | log = logging.getLogger("app." + __name__) 46 | 47 | ######################################################################################################################## 48 | 49 | print('\033c') 50 | if sys.version_info[0] < 3: log.error("Must be using Python 3"); sys.exit(-1) 51 | log.info("Downloading Sonarr Series Data...") 52 | headers = {"Content-type": "application/json", "X-Api-Key": api_key } 53 | url = "{}/sonarr/api/series".format(baseurl) 54 | rsp = requests.get(url, headers=headers) 55 | data = json.loads(rsp.text) 56 | count = 0 57 | for i in data: 58 | headers = {"Content-type": "application/json", "X-Api-Key": api_key } 59 | url = "{}/sonarr/api/series/{}?deleteFiles=false".format(baseurl,i['id']) 60 | # if 'Documentary' in i['genres']: 61 | # print('{} has these genres {}'.format(i['title'],i['genres'])) 62 | if i['episodeFileCount'] == 0: 63 | # print(json.dumps(i, indent=4, sort_keys=True)) 64 | rsp = requests.delete(url, headers=headers) 65 | if rsp.status_code == 200: 66 | count +=1 67 | log.info ("Removing {} from Sonarr...".format(i['title'])) 68 | log.info ("Removed {} Shows.".format(count)) -------------------------------------------------------------------------------- /requirments.txt: -------------------------------------------------------------------------------- 1 | # pip install -r requirments.txt 2 | requests 3 | colorlog -------------------------------------------------------------------------------- /show_list.csv: -------------------------------------------------------------------------------- 1 | title,year,imdbid 2 | 13 Reasons Why,2017,tt1837492 3 | 50 Central,2017,tt7261310 4 | 60 Days In,2016,tt5564124 5 | 9-1-1,2018,tt7235466 6 | A Discovery of Witches,2018,tt2177461 7 | A Million Little Things,2018,tt7608248 8 | A Series of Unfortunate Events,2017,tt4834206 9 | A.P. Bio,2018,tt6461726 10 | Absentia,2017,tt6794990 11 | Active Shooter: America Under Fire,2017,tt7449708 12 | Adam Ruins Everything,2015,tt5034326 13 | Altered Carbon,2018,tt2261227 14 | American Chopper: The Series,2003,tt0364779 15 | American Crime Story,2016,tt2788432 16 | American Gods,2017,tt1898069 17 | American Horror Story,2011,tt1844624 18 | American Idol,2002,tt0319931 19 | Angie Tribeca,2016,tt3597790 20 | Animal Kingdom,2010,tt5574490 21 | Arrow,2012,tt2193021 22 | Atlanta,2016,tt4288182 23 | Ballers,2015,tt2891574 24 | Baskets,2016,tt3468798 25 | Below Deck,2013,tt2342499 26 | Berlin Station,2016,tt5191110 27 | Better Call Saul,2015,tt3032476 28 | Big Little Lies,2017,tt3920596 29 | Bill Nye Saves the World,2017,tt6021260 30 | Billions,2016,tt4270492 31 | Black Lightning,2018,tt6045840 32 | Black Mirror,2011,tt2085059 33 | Black-ish,2014,tt3487356 34 | Blindspot,2015,tt4474344 35 | Blood Ties,2013,tt1747958 36 | Blue Bloods,2010,tt1595859 37 | Body Cam,2018,tt9425082 38 | Bosch,2014,tt3502248 39 | Brooklyn Nine-Nine,2013,tt2467372 40 | Bull,2016,tt5827228 41 | Castle Rock,2018,tt6548228 42 | Charmed,1998,tt0158552 43 | Chicago Fire,2012,tt2261391 44 | Chicago Med,2015,tt4655480 45 | Chicago P.D.,2014,tt2805096 46 | Chilling Adventures of Sabrina,2018,tt7569592 47 | Claws,2017,tt5640558 48 | Cleverman,2016,tt4649420 49 | Cobra Kai,2018,tt7221388 50 | Condor,2018,tt6510950 51 | Container Homes,2016,tt5608176 52 | Coroner,2019,tt8593252 53 | Corporate,2018,tt5648202 54 | Counterpart,2017,tt4643084 55 | Criminal Minds,2005,tt0452046 56 | Dark,2017,tt5753856 57 | DC's Legends of Tomorrow,2016,tt4532368 58 | Deep State,2018,tt4785472 59 | Dirty John,2018,tt7945720 60 | Doom Patrol,2019,tt8416494 61 | Dr. Pimple Popper,2018,tt6045142 62 | Drunk History,2013,tt2712612 63 | Dynasty,2017,tt6128300 64 | Easy,2016,tt5562056 65 | Elementary,2012,tt2191671 66 | Empire,2015,tt3228904 67 | Explained,2018,tt8005374 68 | FantomWorks,2013,tt2951514 69 | Fargo,2014,tt2802850 70 | FBI,2018,tt7491982 71 | Fear Factor,2001,tt0278191 72 | Fear the Walking Dead,2015,tt3743822 73 | Filthy Rich,2016,tt5034426 74 | Flip or Flop Atlanta,2017,tt6597216 75 | For The People,2018,tt6437276 76 | Fresh Off the Boat,2015,tt3551096 77 | Frontier,2016,tt4686698 78 | Fuller House,2016,tt3986586 79 | Future Man,2017,tt4975856 80 | Game of Thrones,2011,tt0944947 81 | Get Shorty,1995,tt0113161 82 | GLOW,2017,tt5770786 83 | God Friended Me,2018,tt7948998 84 | Goliath,2016,tt4687880 85 | Good Girls,2018,tt6474378 86 | Good Trouble,2019,tt7820906 87 | Gotham,2014,tt3749900 88 | Graveyard Carz,2011,tt2078994 89 | Greenleaf,2016,tt4971144 90 | Grey's Anatomy,2005,tt0413573 91 | Grown-ish,2018,tt7018644 92 | Hanna,2011,tt6932244 93 | Happy!,2017,tt2452242 94 | Harlots,2017,tt5761478 95 | Hawaii Five-0,2010,tt1600194 96 | Holmes and Holmes,2016,tt8001366 97 | Homecoming,2018,tt7008682 98 | Homeland,2011,tt1796960 99 | Hot Date,2017,tt7603386 100 | How to Get Away with Murder,2014,tt3205802 101 | I Am Jazz,2015,tt4523638 102 | I Am the Night,2019,tt7186588 103 | I Feel Bad,2018,tt7979042 104 | Impractical Jokers,2011,tt2100976 105 | Impulse,2018,tt6160506 106 | In Contempt,2018,tt7572950 107 | In Search of,1976,tt0074007 108 | In the Dark,2019,tt7772602 109 | Ink Master: Angels,2017,tt7479160 110 | Insecure,2016,tt5024912 111 | Instinct,1999,tt4124758 112 | Into the Badlands,2015,tt3865236 113 | Into the Dark,2018,tt8427140 114 | iZombie,2015,tt3501584 115 | Jane the Virgin,2014,tt3566726 116 | Killing Time,2013,tt3914240 117 | Kingpin,1996,tt0116778 118 | Knightfall,2017,tt4555364 119 | Krypton,2018,tt4276624 120 | Legacies,2018,tt8103070 121 | Legends of the Lost with Megan Fox,2018,tt8362844 122 | Legion,2010,tt1038686 123 | Lethal Weapon,1987,tt0093409 124 | Life Below Zero,2013,tt2964642 125 | Light as a Feather,2018,tt8619822 126 | Liza on Demand,2018,tt7942804 127 | Lost in Space,1998,tt0120738 128 | Lucifer,2015,tt4052886 129 | MacGyver,1985,tt0088559 130 | Madam Secretary,2014,tt3501074 131 | Man with a Plan,2016,tt5536400 132 | Manifest,2018,tt8421350 133 | Mayans M.C.,2018,tt5715524 134 | McMafia,2018,tt6271042 135 | Medal of Honor,2018,tt7440274 136 | "Midnight Texas",2017,tt5464086 137 | Mindhunter,2017,tt5290382 138 | Miracle Workers,2019,tt7529770 139 | Misfit Garage,2014,tt4167914 140 | Modern Family,2009,tt1442437 141 | Mr Inbetween,2018,tt7472896 142 | Mr. Mercedes,2017,tt4354880 143 | Murder Mountain,2018,tt9078908 144 | Murphy Brown,1988,tt0094514 145 | Music City,2018,tt7616830 146 | Narcos: Mexico,2018,tt8714904 147 | NCIS,2003,tt0364845 148 | NCIS: Los Angeles,2009,tt1378167 149 | New Amsterdam,2018,tt7817340 150 | No Activity,2017,tt7230846 151 | Norm Macdonald Has a Show,2018,tt8139862 152 | One Day at a Time,2017,tt5339440 153 | Orange Is the New Black,2013,tt2372162 154 | Origin,2018,tt7801780 155 | Outlander,2014,tt3006802 156 | Ozark,2017,tt5071412 157 | Panic,2000,tt0194218 158 | Patriot Act with Hasan Minhaj,2018,tt8080054 159 | Power,2014,tt3281796 160 | Preacher,2016,tt5016504 161 | Private Eyes,2016,tt5722298 162 | Project Blue Book,2019,tt6632666 163 | Property Brothers,2011,tt1827882 164 | Proven Innocent,2019,tt7958782 165 | Queen of the South,2016,tt1064899 166 | Queen Sugar,2016,tt4419214 167 | Raven's Home,2017,tt6311972 168 | Ray Donovan,2013,tt2249007 169 | Real Country,2018,tt8559538 170 | Rel,2018,tt7677740 171 | Ride with Norman Reedus,2016,tt5547946 172 | Riverdale,2017,tt5420376 173 | Robot Chicken,2005,tt0437745 174 | Room 104,2017,tt6064882 175 | "Roswell, New Mexico",1999,tt0201391 176 | Russian Doll,2019,tt7520794 177 | S.W.A.T.,2003,tt6111130 178 | Saturday Night Live,1975,tt0072562 179 | Schitt's Creek,2015,tt3526078 180 | Schooled,2019,tt6546758 181 | Scream: The TV Series,2015,tt3921180 182 | SEAL Team,2017,tt6473344 183 | Shadowhunters,2016,tt4145054 184 | Shameless,2011,tt1586680 185 | Shark Tank,2009,tt1442550 186 | Sherlock,2010,tt1475582 187 | Showtime at the Apollo,1987,tt0240272 188 | Sideswiped,2018,tt7725756 189 | Silicon Valley,2014,tt2575988 190 | Single Parents,2018,tt7845644 191 | Siren,2018,tt5615700 192 | SMILF,2017,tt6274614 193 | Snatch,2000,tt0208092 194 | Sneaky Pete,2015,tt5011816 195 | Snowfall,2017,tt6439752 196 | Speechless,2016,tt5592146 197 | Splitting Up Together,2018,tt6492236 198 | Lucky Man,2016,tt4800878 199 | Star Trek: Discovery,2017,tt5171438 200 | Station 19,2018,tt7053188 201 | Strange Angel,2018,tt7210448 202 | Stranger Things,2016,tt4574334 203 | Strangers,2007,tt1064215 204 | Succession,2018,tt7660850 205 | Suits,2011,tt1632701 206 | Supergirl,2015,tt4016454 207 | Supernatural,2005,tt0460681 208 | Superstore,2015,tt4477976 209 | Swamp Thing,2019,tt8362852 210 | Talking Dead,2011,tt2089467 211 | Tell Me a Story,2018,tt7695916 212 | The 100,2014,tt2661044 213 | The Big Bang Theory,2007,tt0898266 214 | The Blacklist,2013,tt2741602 215 | The Chi,2018,tt6294706 216 | The Comedy Jam,2017,tt6682226 217 | The Conners,2018,tt8595140 218 | The Cool Kids,2018,tt7725538 219 | The Crown,2016,tt4786824 220 | The Daily Show,2010,tt3661844 221 | The Deuce,2017,tt4998350 222 | The Enemy Within,2019,tt8390342 223 | The Expanse,2015,tt3230854 224 | The Flash,2014,tt3107288 225 | The Gifted,2017,tt4396630 226 | The Goldbergs,2013,tt2712740 227 | The Good Doctor,2017,tt6470478 228 | The Good Fight,2017,tt5853176 229 | The Grand Tour,2016,tt5712554 230 | The Guest Book,2017,tt5990096 231 | The Handmaid's Tale,2017,tt5834204 232 | The Haunting of Hill House,2018,tt6763664 233 | The Innocents,1961,tt0055018 234 | The Jim Jefferies Show,2017,tt6987966 235 | The Kids Are Alright,1979,tt0079400 236 | The Magicians,2015,tt4254242 237 | The Man in the High Castle,2015,tt1740299 238 | The Marvelous Mrs. Maisel,2017,tt5788792 239 | The Masked Singer,2019,tt7670568 240 | The Neighborhood,2018,tt7942794 241 | The Oath,2018,tt7461200 242 | The Orville,2017,tt5691552 243 | The Outpost,2018,tt7612548 244 | The Outsider,2018,tt2011311 245 | The Passage,2019,tt1074206 246 | The Pioneer Woman,2011,tt2151337 247 | The President Show,2017,tt6807442 248 | The Purge,2013,tt2184339 249 | The Ranch,2016,tt4998212 250 | The Resident,2011,tt1334102 251 | The Romanoffs,2018,tt6599482 252 | The Rookie,2002,tt0265662 253 | The Sinner,2017,tt6048596 254 | The Son,2002,tt0291172 255 | The Terror,2018,tt2708480 256 | The Tick,2017,tt5540054 257 | The Titan Games,2019,tt7967262 258 | The Umbrella Academy,2019,tt1312171 259 | The Village,2004,tt0368447 260 | The Voice,2011,tt1839337 261 | The Walking Dead,2010,tt1520211 262 | The World's Best,2019,tt8001752 263 | This Is Us,2016,tt5555260 264 | Timeless,2016,tt5511582 265 | Tin Star,2017,tt4607112 266 | Titans,2018,tt1043813 267 | TKO: Total Knock Out,2018,tt8130392 268 | Tom Clancy's Jack Ryan,2018,tt5057054 269 | Tosh.0,2009,tt1430587 270 | True Detective,2014,tt2356777 271 | Trust,2010,tt1529572 272 | Ultimate Beastmaster,2017,tt5607970 273 | Ultraman,2019,tt8699270 274 | United Shades of America,2016,tt4515676 275 | Van Helsing,2004,tt0338526 276 | Veronica Mars,2004,tt0412253 277 | Vice,2018,tt6266538 278 | Vikings,2013,tt2306299 279 | Watchmen,2009,tt0409459 280 | Wentworth,2013,tt2433738 281 | Westworld,2016,tt0475784 282 | Whiskey Cavalier,2019,tt7599942 283 | Wicked Tuna,2012,tt2191567 284 | Will,2011,tt1636844 285 | Yellowstone,2018,tt4236770 286 | You,2018,tt7335184 287 | Young Sheldon,2017,tt6226232 288 | 3rd Rock from the Sun,1996,tt0115082 289 | 7th Heaven,1996,tt0115083 290 | 90210,2008,tt1225901 291 | A Different World,1987,tt0092339 292 | Adventures of Captain Marvel,1941,tt0033317 293 | Airwolf,1984,tt0086662 294 | Alice,2008,tt1447826 295 | All in the Family,1971,tt0066626 296 | All That,1994,tt0111875 297 | Amen,2013,tt2786278 298 | B.J. and the Bear,1978,tt0078564 299 | Baretta,1975,tt0072471 300 | Barnaby Jones,1973,tt0069557 301 | Batman,1989,tt0096895 302 | Baywatch,2017,tt1469304 303 | Beauty and the Beast,1991,tt0101414 304 | Benson,1979,tt0078569 305 | Bewitched,2005,tt0374536 306 | Blossom,1990,tt0101050 307 | Bosom Buddies,1980,tt0080202 308 | Buffy the Vampire Slayer,1997,tt0118276 309 | Cagney & Lacey,1981,tt0083395 310 | Charles in Charge,1984,tt0086681 311 | Charlie's Angels,1976,tt0073972 312 | Cheers,1982,tt0083399 313 | Chicago Hope,1994,tt0108724 314 | China Beach,1988,tt0094433 315 | CHIPS,2017,tt0493405 316 | Columbo,1971,tt1466074 317 | Dad's Army,1968,tt0062552 318 | Designing Women,1986,tt0090418 319 | Diff'rent Strokes,1978,tt0077003 320 | Dinosaurs,1991,tt0101081 321 | "Dr. Quinn, Medicine Woman",1993,tt0103405 322 | Everybody Loves Raymond,1996,tt0115167 323 | Family Matters,1989,tt0096579 324 | Fantasy Island,1977,tt0077008 325 | Father Knows Best,1954,tt0046600 326 | Fraggle Rock,1983,tt0085017 327 | Freddy's Nightmares,1988,tt0094466 328 | Friends,1994,tt0108778 329 | Full House,1987,tt0092359 330 | Galactica 1980,1980,tt0080221 331 | Get Smart,2008,tt0425061 332 | Good Times,1974,tt0070991 333 | Grace Under Fire,1993,tt0106017 334 | Green Acres,1965,tt0058808 335 | Hammer House of Horror,1980,tt0080231 336 | Happy Days,1974,tt0070992 337 | Hawaii Five-O,1968,tt0062568 338 | Hearts Afire,1992,tt0103437 339 | Highway to Heaven,1984,tt0086730 340 | Home Improvement,1991,tt0101120 341 | Hooperman,1987,tt0092373 342 | Hunter,1984,tt0086734 343 | I Dream of Jeannie,1965,tt0058815 344 | I Love Lucy,1951,tt0043208 345 | In Living Color,1990,tt0098830 346 | In the Heat of the Night,1967,tt0061811 347 | Johnny Sokko and His Flying Robot,1967,tt0170962 348 | Kenan & Kel,2008,tt0115231 349 | Knight Rider,1982,tt0083437 350 | Kung Fu,1972,tt0068093 351 | Lassie,1994,tt0110305 352 | Legends of the Superheroes,1979,tt0131675 353 | Little House on the Prairie,1974,tt0071007 354 | Mama's Family,1983,tt0085050 355 | Martin,1977,tt0077914 356 | M-A-S-H,1970,tt0066026 357 | Matlock,1986,tt0090481 358 | Matt Houston,1982,tt0083447 359 | Miami Vice,2006,tt0430357 360 | Mister Ed,1958,tt0054557 361 | Moesha,1996,tt0115275 362 | Moonlighting,1985,tt0088571 363 | Mork & Mindy,1978,tt0077053 364 | Mr. Bean,1990,tt0096657 365 | "Murder, She Wrote",1984,tt0086765 366 | Mystery Science Theater 3000,1988,tt0094517 367 | Newhart,1982,tt0083455 368 | NYPD Blue,1993,tt0106079 369 | One Day at a Time,2017,tt5339440 370 | Perfect Strangers,2016,tt4901306 371 | Perry Mason,1957,tt0050051 372 | Police Woman,1974,tt0071034 373 | Quantum Leap,1989,tt0096684 374 | Remington Steele,1982,tt0083470 375 | Sanford and Son,1972,tt0068128 376 | Saved by the Bell,1989,tt0096694 377 | Scarecrow and Mrs. King,1983,tt0085088 378 | Silver Spoons,1982,tt0083479 379 | Simon,2004,tt0393775 380 | Soap,1977,tt0075584 381 | St. Elsewhere,1982,tt0083483 382 | Star Trek: Deep Space Nine,1993,tt0106145 383 | Star Trek: Voyager,1995,tt0112178 384 | Step by Step,1991,tt0101205 385 | Street Hawk,1985,tt0088618 386 | Tales from the Crypt,1989,tt0096708 387 | Terrahawks,1983,tt0085099 388 | That '70s Show,1998,tt0165598 389 | The A-Team,2010,tt0429493 390 | The Alfred Hitchcock Hour,1962,tt0055657 391 | The Amazing Spider-Man,2012,tt0948470 392 | The Andy Griffith Show,1960,tt0053479 393 | The Beverly Hillbillies,1993,tt0106400 394 | The Bionic Woman,1976,tt0073965 395 | The Bob Newhart Show,1972,tt0068049 396 | The Brady Bunch,1969,tt0063878 397 | The Carol Burnett Show,1967,tt0061240 398 | The Commish,1991,tt0101069 399 | The Cosby Show,1984,tt0086687 400 | The Dana Carvey Show,1996,tt0115148 401 | The Dick Van Dyke Show,1961,tt0054533 402 | The Dukes of Hazzard,2005,tt0377818 403 | The Equalizer,2014,tt0455944 404 | The Facts of Life,1979,tt0078610 405 | The Fall Guy,1981,tt0081859 406 | The Fresh Prince of Bel-Air,1990,tt0098800 407 | The Golden Girls,1985,tt0088526 408 | The Green Hornet,2011,tt0990407 409 | The Honeymooners,2005,tt0373908 410 | The Incredible Hulk,2008,tt0800080 411 | The Jeff Foxworthy Show,1995,tt0112025 412 | The Kids in the Hall,1988,tt0096626 413 | The Life and Times of Grizzly Adams,1977,tt0075525 414 | The Love Boat,1977,tt0075529 415 | The Lucy Show,1962,tt0055686 416 | The Martian Chronicles,1980,tt0080242 417 | The Munsters,1964,tt0057773 418 | The Muppet Show,1976,tt0074028 419 | The Nanny,1993,tt0106080 420 | The Prisoner,1967,tt0061287 421 | The Rockford Files,1974,tt0071042 422 | The Six Million Dollar Man,1974,tt0071054 423 | The State,1993,tt0130421 424 | The Three Stooges,2012,tt0383010 425 | The Twilight Zone,1959,tt0052520 426 | The Waltons,1971,tt0068149 427 | The Wayans Bros.,1995,tt0112220 428 | The Wonder Years,1988,tt0094582 429 | Three's Company,1976,tt0075596 430 | Thunderbirds,2004,tt0167456 431 | Unsolved Mysteries,1987,tt0094574 432 | Viper,1994,tt0108983 433 | "Walker, Texas Ranger",1993,tt0106168 434 | Webster,1983,tt0085109 435 | "Welcome Back, Kotter",1975,tt0072582 436 | Whos the Boss,1984,tt0086827 437 | Window on Main Street,1961,tt0054579 438 | Wings,1927,tt0018578 439 | WKRP in Cincinnati,1978,tt0077097 440 | Wonder Woman,2017,tt0451279 441 | A Discovery of Witches,2018,tt2177461 442 | An Idiot Abroad,2010,tt1702042 443 | Bread,1986,tt0090402 444 | Crazyhead,2016,tt6038584 445 | Criminal Justice,2019,tt9095260 446 | Downton Abbey,2010,tt1606375 447 | Friday Night Dinner,2011,tt1844923 448 | George and Mildred,1980,tt0138464 449 | Good Omens,2019,tt1869454 450 | Harrow,2018,tt7242816 451 | Humans,2015,tt4122068 452 | It Ain't Half Hot Mum,1974,tt0081878 453 | Jamestown,2017,tt5650650 454 | Love Thy Neighbour,1972,tt0068096 455 | Luther,2010,tt1474684 456 | Merlin,2008,tt1199099 457 | Only Fools and Horses,1981,tt0081912 458 | Peaky Blinders,2013,tt2442560 459 | Phoenix Nights,2001,tt0273379 460 | Plebs,2013,tt2731624 461 | Sherlock,2010,tt1475582 462 | Some Mothers Do 'Ave 'Em,1973,tt0069634 463 | The ABC Murders,2018,tt8463714 464 | The Frankenstein Chronicles,2015,tt4206804 465 | The Inbetweeners,2008,tt1220617 466 | The Little Drummer Girl,2018,tt7598448 467 | The Mighty Boosh,2003,tt0416394 468 | Toast of London,2012,tt2432604 469 | Two Pints of Lager and a Packet of Crisps,2001,tt0278251 470 | Bonanza,1959,tt0052451 471 | Comanche Moon,2008,tt0783328 472 | Deadwood,2004,tt0348914 473 | Gunslingers,2014,tt3480830 474 | Gunsmoke,1955,tt0047736 475 | Have Gun - Will Travel,1957,tt0050025 476 | Hell on Wheels,2011,tt1699748 477 | Hopalong Cassidy,1952,tt0041030 478 | Tales of Wells Fargo,1957,tt0050066 479 | The American West,2016,tt3738872 480 | The Lone Ranger,2013,tt1210819 481 | The Sacketts,1979,tt0079840 482 | The Virginian,1962,tt0055710 483 | Trackdown,1957,tt0050071 484 | Wagon Train,1957,tt0050073 485 | Wanted: Dead or Alive,1986,tt0094293 -------------------------------------------------------------------------------- /shows: -------------------------------------------------------------------------------- 1 | 13 Reasons Why 2 | 50 Central 3 | 60 Days In 4 | 9-1-1 5 | A Discovery of Witches 6 | A Million Little Things 7 | A Series of Unfortunate Events 8 | A.P. Bio 9 | Absentia 10 | Active Shooter: America Under Fire 11 | Adam Ruins Everything 12 | Altered Carbon 13 | American Chopper: The Series 14 | American Crime Story 15 | American Gods 16 | American Horror Story 17 | American Idol 18 | Angie Tribeca 19 | Animal Kingdom 20 | Arrow 21 | Atlanta 22 | Ballers 23 | Baskets 24 | Below Deck 25 | Berlin Station 26 | Better Call Saul 27 | Big Little Lies 28 | Bill Nye Saves the World 29 | Billions 30 | Black Lightning 31 | Black Mirror 32 | Black-ish 33 | Blindspot 34 | Blood Ties 35 | Blue Bloods 36 | Body Cam 37 | Bosch 38 | Brooklyn Nine-Nine 39 | Bull 40 | Castle Rock 41 | Charmed 42 | Chicago Fire 43 | Chicago Med 44 | Chicago P.D. 45 | Chilling Adventures of Sabrina 46 | Claws 47 | Cleverman 48 | Cobra Kai 49 | Condor 50 | Container Homes 51 | Coroner 52 | Corporate 53 | Counterpart 54 | Criminal Minds 55 | Dark 56 | DC's Legends of Tomorrow 57 | Deep State 58 | Dirty John 59 | Doom Patrol 60 | Dr. Pimple Popper 61 | Drunk History 62 | Dynasty 63 | Easy 64 | Elementary 65 | Empire 66 | Explained 67 | FantomWorks 68 | Fargo 69 | FBI 70 | Fear Factor 71 | Fear the Walking Dead 72 | Filthy Rich 73 | Flip or Flop Atlanta 74 | For The People 75 | Fresh Off the Boat 76 | Frontier 77 | Fuller House 78 | Future Man 79 | Game of Thrones 80 | Genius 81 | Get Shorty 82 | GLOW 83 | God Friended Me 84 | Goliath 85 | Good Girls 86 | Good Trouble 87 | Gotham 88 | Graveyard Carz 89 | Greenleaf 90 | Grey's Anatomy 91 | Grown-ish 92 | Hanna 93 | Happy! 94 | Harlots 95 | Hawaii Five-0 96 | Holmes and Holmes 97 | Homecoming 98 | Homeland 99 | Hot Date 100 | How to Get Away with Murder 101 | I Am Jazz 102 | I Am the Night 103 | I Feel Bad 104 | Impractical Jokers 105 | Impulse 106 | In Contempt 107 | In Search of 108 | In the Dark 109 | Ink Master: Angels 110 | Insecure 111 | Instinct 112 | Into the Badlands 113 | Into the Dark 114 | iZombie 115 | Jane the Virgin 116 | Killing Time 117 | Kingpin 118 | Knightfall 119 | Krypton 120 | Legacies 121 | Legends of the Lost with Megan Fox 122 | Legion 123 | Lethal Weapon 124 | Life Below Zero 125 | Light as a Feather 126 | Liza on Demand 127 | Lost in Space 128 | Lucifer 129 | MacGyver 130 | Madam Secretary 131 | Man with a Plan 132 | Manifest 133 | Mayans M.C. 134 | McMafia 135 | Medal of Honor 136 | "Midnight, Texas" 137 | Mindhunter 138 | Miracle Workers 139 | Misfit Garage 140 | Modern Family 141 | Mr Inbetween 142 | Mr. Mercedes 143 | Murder Mountain 144 | Murphy Brown 145 | Music City 146 | Narcos: Mexico 147 | NCIS 148 | NCIS: Los Angeles 149 | New Amsterdam 150 | No Activity 151 | Norm Macdonald Has a Show 152 | One Day at a Time 153 | Orange Is the New Black 154 | Origin 155 | Outlander 156 | Ozark 157 | Panic 158 | Patriot Act with Hasan Minhaj 159 | Power 160 | Preacher 161 | Private Eyes 162 | Project Blue Book 163 | Property Brothers 164 | Proven Innocent 165 | Queen of the South 166 | Queen Sugar 167 | Raven's Home 168 | Ray Donovan 169 | Real Country 170 | Rel 171 | Ride with Norman Reedus 172 | Riverdale 173 | Robot Chicken 174 | Room 104 175 | "Roswell, New Mexico" 176 | Russian Doll 177 | S.W.A.T. 178 | Saturday Night Live 179 | Schitt's Creek 180 | Schooled 181 | Scream: The TV Series 182 | SEAL Team 183 | Shadowhunters 184 | Shameless 185 | Shark Tank 186 | Sherlock 187 | Showtime at the Apollo 188 | Sideswiped 189 | Silicon Valley 190 | Single Parents 191 | Siren 192 | SMILF 193 | Snatch 194 | Sneaky Pete 195 | Snowfall 196 | Speechless 197 | Splitting Up Together 198 | Stan Lee's Lucky Man 199 | Star Trek: Discovery 200 | Station 19 201 | Strange Angel 202 | Stranger Things 203 | Strangers 204 | Succession 205 | Suits 206 | Supergirl 207 | Supernatural 208 | Superstore 209 | Swamp Thing 210 | Talking Dead 211 | Tell Me a Story 212 | The 100 213 | The Big Bang Theory 214 | The Blacklist 215 | The Chi 216 | The Comedy Jam 217 | The Conners 218 | The Cool Kids 219 | The Crown 220 | The Daily Show 221 | The Deuce 222 | The Enemy Within 223 | The Expanse 224 | The Flash 225 | The Gifted 226 | The Goldbergs 227 | The Good Doctor 228 | The Good Fight 229 | The Grand Tour 230 | The Guest Book 231 | The Handmaid's Tale 232 | The Haunting of Hill House 233 | The Innocents 234 | The Jim Jefferies Show 235 | The Kids Are Alright 236 | The Magicians 237 | The Man in the High Castle 238 | The Marvelous Mrs. Maisel 239 | The Masked Singer 240 | The Neighborhood 241 | The Oath 242 | The Orville 243 | The Outpost 244 | The Outsider 245 | The Passage 246 | The Pioneer Woman 247 | The President Show 248 | The Purge 249 | The Ranch 250 | The Resident 251 | The Romanoffs 252 | The Rookie 253 | The Sinner 254 | The Son 255 | The Terror 256 | The Tick 257 | The Titan Games 258 | The Umbrella Academy 259 | The Village 260 | The Voice 261 | The Walking Dead 262 | The World's Best 263 | This Is Us 264 | Timeless 265 | Tin Star 266 | Titans 267 | TKO: Total Knock Out 268 | Tom Clancy's Jack Ryan 269 | Tosh.0 270 | True Detective 271 | Trust 272 | Ultimate Beastmaster 273 | Ultraman 274 | United Shades of America 275 | Van Helsing 276 | Veronica Mars 277 | Vice 278 | Vikings 279 | Watchmen 280 | Wentworth 281 | Westworld 282 | Whiskey Cavalier 283 | Wicked Tuna 284 | Will 285 | Yellowstone 286 | You 287 | Young Sheldon 288 | 3rd Rock from the Sun 289 | 7th Heaven 290 | 90210 291 | A Different World 292 | Airwolf 293 | Alice 294 | All in the Family 295 | All That 296 | Amen 297 | B.J. and the Bear 298 | Baretta 299 | Barnaby Jones 300 | Batman 301 | Baywatch 302 | Beauty and the Beast 303 | Benson 304 | Bewitched 305 | Blossom 306 | Bosom Buddies 307 | Buffy the Vampire Slayer 308 | Cagney & Lacey 309 | Charles in Charge 310 | Charlie's Angels 311 | Cheers 312 | Chicago Hope 313 | China Beach 314 | CHIPS 315 | Columbo 316 | Dad's Army 317 | Designing Women 318 | Diff'rent Strokes 319 | Dinosaurs 320 | "Dr. Quinn, Medicine Woman" 321 | Everybody Loves Raymond 322 | Family Matters 323 | Fantasy Island 324 | Father Knows Best 325 | Fraggle Rock 326 | Freddy's Nightmares 327 | Friends 328 | Full House 329 | Galactica 1980 330 | Get Smart 331 | Good Times 332 | Grace Under Fire 333 | Green Acres 334 | Hammer House of Horror 335 | Happy Days 336 | Hawaii Five-O 337 | Hearts Afire 338 | Highway to Heaven 339 | Home Improvement 340 | Hooperman 341 | Hunter 342 | I Dream of Jeannie 343 | I Love Lucy 344 | In Living Color 345 | In the Heat of the Night 346 | Johnny Sokko and His Flying Robot 347 | Kenan & Kel 348 | Knight Rider 349 | Kung Fu 350 | Lassie 351 | Legends of the Superheroes 352 | Little House on the Prairie 353 | Mama's Family 354 | Martin 355 | M-A-S-H 356 | Matlock 357 | Matt Houston 358 | Miami Vice 359 | Mister Ed 360 | Moesha 361 | Moonlighting 362 | Mork & Mindy 363 | Mr. Bean 364 | "Murder, She Wrote" 365 | Mystery Science Theater 3000 366 | Newhart 367 | NYPD Blue 368 | One Day at a Time 369 | Perfect Strangers 370 | Perry Mason 371 | Police Woman 372 | Quantum Leap 373 | Remington Steele 374 | Sanford and Son 375 | Saved by the Bell 376 | Scarecrow and Mrs. King 377 | Silver Spoons 378 | Simon 379 | Soap 380 | St. Elsewhere 381 | Star Trek: Deep Space Nine 382 | Star Trek: Voyager 383 | Step by Step 384 | Street Hawk 385 | Tales from the Crypt 386 | Terrahawks 387 | That '70s Show 388 | The A-Team 389 | The Alfred Hitchcock Hour 390 | The Amazing Spider-Man 391 | The Andy Griffith Show 392 | The Beverly Hillbillies 393 | The Bionic Woman 394 | The Bob Newhart Show 395 | The Brady Bunch 396 | The Carol Burnett Show 397 | The Commish 398 | The Cosby Show 399 | The Dana Carvey Show 400 | The Dick Van Dyke Show 401 | The Dukes of Hazzard 402 | The Equalizer 403 | The Facts of Life 404 | The Fall Guy 405 | The Fresh Prince of Bel-Air 406 | The Golden Girls 407 | The Green Hornet 408 | The Honeymooners 409 | The Incredible Hulk 410 | The Jeff Foxworthy Show 411 | The Kids in the Hall 412 | The Life and Times of Grizzly Adams 413 | The Love Boat 414 | The Lucy Show 415 | The Martian Chronicles 416 | The Munsters 417 | The Muppet Show 418 | The Nanny 419 | The Prisoner 420 | The Rockford Files 421 | The Six Million Dollar Man 422 | The State 423 | The Three Stooges 424 | The Twilight Zone 425 | The Waltons 426 | The Wayans Bros. 427 | The Wonder Years 428 | Three's Company 429 | Thunderbirds 430 | Unsolved Mysteries 431 | Viper 432 | "Walker, Texas Ranger" 433 | Webster 434 | "Welcome Back, Kotter" 435 | Who's the Boss 436 | Window on Main Street 437 | Wings 438 | WKRP in Cincinnati 439 | Wonder Woman 440 | A Discovery of Witches 441 | An Idiot Abroad 442 | Bread 443 | Crazyhead 444 | Criminal Justice 445 | Downton Abbey 446 | Friday Night Dinner 447 | George & Mildred 448 | Good Omens 449 | Harrow 450 | Humans 451 | It Ain't Half Hot Mum 452 | Jamestown 453 | Love Thy Neighbour 454 | Luther 455 | Merlin 456 | Only Fools and Horses 457 | Peaky Blinders 458 | Phoenix Nights 459 | Plebs 460 | Sherlock 461 | Some Mothers Do 'Ave 'Em 462 | The ABC Murders 463 | The Frankenstein Chronicles 464 | The Inbetweeners 465 | The Little Drummer Girl 466 | The Mighty Boosh 467 | Toast of London 468 | Two Pints of Lager and a Packet of Crisps 469 | Bonanza 470 | Comanche Moon 471 | Deadwood 472 | Gunslingers 473 | Gunsmoke 474 | Have Gun - Will Travel 475 | Hell on Wheels 476 | Hopalong Cassidy 477 | Tales of Wells Fargo 478 | The American West 479 | The Lone Ranger 480 | The Sacketts 481 | The Virginian 482 | Trackdown 483 | Wagon Train 484 | Wanted: Dead or Alive -------------------------------------------------------------------------------- /sonarr_add_from_list.py: -------------------------------------------------------------------------------- 1 | import os, time, requests, logging, logging.handlers, json, sys, re, csv, configparser, base64 2 | from colorlog import ColoredFormatter 3 | from datetime import datetime 4 | 5 | show_added_count = 0 6 | show_exist_count = 0 7 | sonarrData = [] 8 | # Config ############################################################################################################### 9 | 10 | config = configparser.ConfigParser() 11 | config.read('./config.ini') 12 | baseurl = config['sonarr']['baseurl'] 13 | urlbase = config['sonarr']['urlbase'] 14 | api_key = config['sonarr']['api_key'] 15 | rootfolderpath = config['sonarr']['rootfolderpath'] 16 | searchForShow = config['sonarr']['searchForShow'] 17 | qualityProfileId = config['sonarr']['qualityProfileId'] 18 | omdbapi_key = config['sonarr']['omdbapi_key'] 19 | 20 | # Logging ############################################################################################################## 21 | 22 | logging.getLogger().setLevel(logging.NOTSET) 23 | 24 | formatter = ColoredFormatter( 25 | "%(log_color)s[%(levelname)s]%(reset)s %(white)s%(message)s", 26 | datefmt=None, 27 | reset=True, 28 | log_colors={ 29 | 'DEBUG': 'cyan', 30 | 'INFO': 'green', 31 | 'WARNING': 'yellow', 32 | 'ERROR': 'red', 33 | 'CRITICAL': 'red,bg_white', 34 | }, 35 | secondary_log_colors={}, 36 | style='%' 37 | ) 38 | 39 | logger = logging.StreamHandler() 40 | logger.setLevel(logging.INFO) # DEBUG To show all 41 | logger.setFormatter(formatter) 42 | logging.getLogger().addHandler(logger) 43 | 44 | if not os.path.exists("./logs/"): os.mkdir("./logs/") 45 | logFileName = "./logs/safl.log" 46 | filelogger = logging.handlers.RotatingFileHandler(filename=logFileName) 47 | filelogger.setLevel(logging.DEBUG) 48 | logFormatter = logging.Formatter("%(asctime)s [%(levelname)-5.5s] %(message)s") 49 | filelogger.setFormatter(logFormatter) 50 | logging.getLogger().addHandler(filelogger) 51 | 52 | log = logging.getLogger("app." + __name__) 53 | 54 | ######################################################################################################################## 55 | 56 | def add_show(title,year,imdbid): 57 | 58 | # Add Missing to sonarr Work in Progress 59 | global show_added_count 60 | global show_exist_count 61 | if imdbid == None : imdbid = get_imdbid(title,year) 62 | if imdbid == None : log.info("Not imdbid found for {}".format(title)); return 63 | imdbIds = [] 64 | tvdbIds = [] 65 | for shows_to_add in sonarrData: imdbIds.append(shows_to_add.get('imdbId')) 66 | for shows_to_add in sonarrData: tvdbIds.append(shows_to_add.get('tvdbId')) 67 | 68 | if imdbid not in imdbIds: 69 | tvdbId = get_tvdbId(title,imdbid) 70 | if tvdbId in tvdbIds: 71 | show_exist_count +=1 72 | log.info("\033[1;36m{}\t {} ({}) already Exists in Sonarr.\u001b[0m".format(imdbid,title,year)) 73 | return 74 | session = requests.Session() 75 | adapter = requests.adapters.HTTPAdapter(max_retries=20) 76 | session.mount('https://', adapter) 77 | session.mount('http://', adapter) 78 | 79 | if tvdbId == None: log.error("No tvdbId found for {}".format(title)); return 80 | if not qualityProfileId.isdigit(): 81 | ProfileId = get_profile_from_id(qualityProfileId) 82 | elif qualityProfileId == None: 83 | log.error("\u001b[35m qualityProfileId Not Set in the config correctly.\u001b[0m") 84 | else: 85 | ProfileId = qualityProfileId 86 | 87 | headers = {"Content-type": "application/json"} 88 | url = "{}{}/api/v3/series/lookup?term=tvdb:{}&apikey={}".format(baseurl,urlbase,tvdbId, api_key ) 89 | rsp = session.get(url, headers=headers) 90 | data = json.loads(rsp.text) 91 | if rsp.text =="[]": 92 | log.error("\u001b[35mSorry. We couldn't find {} ({})\u001b[0m".format(title,year)) 93 | return 94 | if len(rsp.text)==0: 95 | log.error("Sorry. We couldn't find any Shows matching {} ({})".format(title,year)) 96 | return 97 | tvdbId = data[0]["tvdbId"] 98 | title = data[0]["title"] 99 | year = data[0]["year"] 100 | images = json.loads(json.dumps(data[0]["images"])) 101 | titleslug = data[0]["titleSlug"] 102 | seasons = json.loads(json.dumps(data[0]["seasons"])) 103 | headers = {"Content-type": "application/json", "X-Api-Key": "{}".format(api_key)} 104 | data = json.dumps({ 105 | "title": title , 106 | "year": year , 107 | "tvdbId": tvdbId , 108 | "titleslug": titleslug, 109 | "monitored": True , 110 | "seasonFolder": True, 111 | "qualityProfileId": ProfileId, 112 | "rootFolderPath": rootfolderpath , 113 | "images": images, 114 | "seasons": seasons, 115 | "addOptions": 116 | { 117 | "ignoreEpisodesWithFiles": True, 118 | "ignoreEpisodesWithoutFiles": False, 119 | "searchForMissingEpisodes": bool(searchForShow) 120 | } 121 | 122 | }) 123 | 124 | url = '{}{}/api/v3/series'.format(baseurl,urlbase) 125 | rsp = requests.post(url, headers=headers, data=data) 126 | data = json.loads(rsp.text) 127 | 128 | if rsp.status_code == 201: 129 | show_added_count +=1 130 | if searchForShow == "True": 131 | log.info("\033[0;32m{}\t {} ({}) Added to Sonarr :) Now Searching.\u001b[0m".format(imdbid,title,year)) 132 | else: 133 | log.info("\033[0;32m{}\t {} ({}) Added to Sonarr :) \033[1;31mSearch Disabled.\u001b[0m".format(imdbid,title,year)) 134 | elif rsp.status_code == 400: 135 | show_exist_count +=1 136 | log.info("\033[1;36m{}\t {} ({}) already Exists in Sonarr.\u001b[0m".format(imdbid,title,year)) 137 | return 138 | else: 139 | log.error("\u001b[32m{}\t {} ({}) Not found, Not added to Sonarr.\u001b[0m".format(imdbid,title,year)) 140 | return 141 | 142 | else: 143 | show_exist_count+=1 144 | log.info("\033[1;36m{}\t {} ({}) already Exists in Sonarr.\u001b[0m".format(imdbid,title,year)) 145 | return 146 | 147 | def get_imdbid(title,year): 148 | # Get TV Show imdbid 149 | headers = {"Content-type": "application/json", 'Accept':'application/json'} 150 | r = requests.get("https://www.omdbapi.com/?t={}&y={}&apikey={}".format(title,year,omdbapi_key), headers=headers) 151 | if r.status_code == 401: 152 | log.error("omdbapi Request limit reached!") 153 | return None 154 | 155 | if r.status_code == 200: 156 | d = json.loads(r.text) 157 | if d.get('Response') == "False": 158 | return None 159 | else: 160 | return d.get('imdbID') 161 | else: 162 | return None 163 | 164 | def get_tvdbId(title,imdbid): 165 | api = str(base64.b64decode('YWE2Yjc5YTBlZDdjM2Y3NWUyOWI1MjkyOTAyNjhmOGFkNzM0ZmE3MWUzYzA3Zjg2YmE2OTVlMzQzZDFmZmNjMw==')) 166 | title = title.replace(" ","-"); title = title.replace("'","-"); title = title.replace(":","") 167 | if title.find("&"): title = title.replace(" ",""); title = title.replace("&","-") 168 | headers = {'Content-Type': 'application/json', 'trakt-api-version': '2', 'trakt-api-key': api} 169 | rsp = requests.get('https://api.trakt.tv/search/imdb/{}?type=show'.format(imdbid), headers=headers) 170 | if rsp.status_code == 403: log.error("trakt Api Failed"); return None 171 | if rsp.status_code == 200: 172 | d = json.loads(rsp.text) 173 | if d == []: 174 | rsp = requests.get('https://api.trakt.tv/shows/{}'.format(title), headers=headers) 175 | if rsp.status_code == 200: 176 | d = json.loads(rsp.text) 177 | if d == []: return None 178 | return d['ids']['tvdb'] 179 | else: 180 | return None 181 | else: 182 | return d[0]['show']['ids']['tvdb'] 183 | else: 184 | return None 185 | 186 | def get_profile_from_id(id): 187 | headers = {"Content-type": "application/json", "X-Api-Key": "{}".format(api_key)} 188 | url = "{}{}/api/v3/profile".format(baseurl,urlbase) 189 | r = requests.get(url, headers=headers) 190 | d = json.loads(r.text) 191 | profile = next((item for item in d if item["name"].lower() == id.lower()), False) 192 | if not profile: 193 | log.error('Could not find profile_id for instance profile {}'.format(id)) 194 | sys.exit(0) 195 | return profile.get('id') 196 | 197 | 198 | def main(): 199 | print('\033c') 200 | if sys.version_info[0] < 3: log.error("Must be using Python 3"); sys.exit(-1) 201 | global sonarrData 202 | if len(sys.argv)<2: log.error("No list Specified... Bye!!"); sys.exit(-1) 203 | if not os.path.exists(sys.argv[1]): log.info("{} Does Not Exist".format(sys.argv[1])); sys.exit(-1) 204 | log.info("Downloading Sonarr Show Data. :)") 205 | headers = {"Content-type": "application/json", "X-Api-Key": api_key } 206 | url = "{}{}/api/v3/series".format(baseurl,urlbase) 207 | rsp = requests.get(url , headers=headers) 208 | if rsp.status_code == 200: 209 | sonarrData = json.loads(rsp.text) 210 | else: 211 | log.error("Failed to connect to Radar...") 212 | with open(sys.argv[1], encoding="utf8") as csvfile: total_count = len(list(csv.DictReader(csvfile))) 213 | with open(sys.argv[1], encoding="utf8") as csvfile: 214 | s = csv.DictReader(csvfile) 215 | if not total_count>0: log.error("No TV Shows Found in file... Bye!!"); exit() 216 | log.info("Found {} TV Shows in {}. :)".format(total_count,sys.argv[1])) 217 | 218 | for row in s: 219 | if not (row): continue 220 | if len(row) >= 4: log.error("Invalid Format on line {} Data:{}".format(str(s.line_num),row)); continue 221 | try: row['title'] 222 | except: log.error("Invalid CSV File, Header does not contain title,year,imdbid"); sys.exit(-1) 223 | title = row['title']; year = row['year']; imdbid = row['imdbid'] 224 | try: add_show(title,year,imdbid) 225 | except Exception as e: log.error(e); sys.exit(-1) 226 | log.info("Added {} of {} Shows, {} Already Exist".format(show_added_count,total_count,show_exist_count)) 227 | 228 | 229 | if __name__ == "__main__": 230 | main() 231 | 232 | --------------------------------------------------------------------------------