Compare commits

...

159 Commits

Author SHA1 Message Date
github-actions[bot] 97ea3a8124
Merge development into master 2024-06-02 14:21:09 +00:00
Alex Meyer 77302fad21
Fixed throttled_providers.dat reset 2024-05-30 22:16:24 -04:00
Anderson Shindy Oki b7e6de71ff
Fixed bazarr restart traceback exception 2024-05-30 22:08:29 -04:00
JayZed 884200441b
Fix for case insensitive filesystem upates
This fix was made necessary when a library changed the case of one of its files, but kept the name the same.
When the file was updated in place, the case did not change.
The solution is to delete the file first before extracting the new one from the zip file with the changed case.
2024-05-27 21:18:45 -04:00
morpheus65535 0e183c428b Fixed subdivx series search process. #2499 2024-05-27 20:58:29 -04:00
morpheus65535 ebb0cc16b1
no log: Delete libs/apprise/apprise.pyi 2024-05-25 00:19:06 -04:00
morpheus65535 0abf56191c
no log: Delete libs/apprise/apprise.py 2024-05-25 00:18:49 -04:00
morpheus65535 5ca733eac0 Reverted to apprise 1.7.6 to fix an issue with the upgrade process first. 1.8.0 will get back in nightly shortly. #2497 2024-05-24 13:19:37 -04:00
morpheus65535 3e929d8ef9 Fixed upgrade process that was broken since Apprise 1.8.0 update. #2497 2024-05-23 20:46:07 -04:00
dependabot[bot] 07534282a2
no log: Bump @types/lodash from 4.17.0 to 4.17.1 in /frontend (#2495)
Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.17.0 to 4.17.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

---
updated-dependencies:
- dependency-name: "@types/lodash"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-13 22:24:04 -04:00
morpheus65535 d8e58cac83 no log: fixed empty subtitles being saved 2024-05-13 22:11:40 -04:00
morpheus65535 811394cec3
no log: Delete libs/apprise/Apprise.py 2024-05-12 23:16:17 -04:00
morpheus65535 9cb2708909
no log: Delete libs/apprise/Apprise.pyi 2024-05-12 23:16:01 -04:00
morpheus65535 d70a92e947 Fixed uppercase issue in Apprise module name. 2024-05-12 23:13:21 -04:00
morpheus65535 b3a5d43a10 Fixed issue while saving some odd case ASS embedded subtitles. 2024-05-12 10:13:21 -04:00
morpheus65535 fd0a8c3d3b Emergency fix following Apprise 1.8.0 upgrade 2024-05-12 10:11:43 -04:00
morpheus65535 86d34039a3 Updated apprise to 1.8.0 2024-05-11 23:22:55 -04:00
morpheus65535 006ee0f63a Fixed issue with subssabbz provider comparing None with int. 2024-05-10 06:46:50 -04:00
morpheus65535 47011f429a Fixed issue with subsunacs provider comparing None with int. 2024-05-10 06:36:04 -04:00
morpheus65535 485122bfae no log: removing leftover subscene remnants 2024-05-09 15:27:48 -04:00
morpheus65535 bb4b01f3fb Removed closed subscene provider 2024-05-09 15:19:31 -04:00
morpheus65535 f914ed0cbf Merge remote-tracking branch 'origin/development' into development 2024-05-08 23:37:11 -04:00
Anderson Shindy Oki 5b5beadf4d Removed dependency over moment library
* feat: remove moment dependency

* refactor

* add tests

* small format

* rename argument
2024-05-08 23:36:50 -04:00
Anderson Shindy Oki 6e3422524c
Removed dependency over moment
* feat: remove moment dependency

* refactor

* add tests

* small format

* rename argument
2024-05-08 23:35:41 -04:00
morpheus65535 014ba07aea Merge remote-tracking branch 'origin/development' into development 2024-05-08 22:29:34 -04:00
morpheus65535 4815313ac6 Fixed db migrations dropping tables content because of ForeignKey constraints. #2489 2024-05-08 22:29:31 -04:00
Anderson Shindy Oki 397310eff5
no log: Fix husky installation (#2488) 2024-05-07 20:32:17 -04:00
morpheus65535 d686ab71b2 Merge remote-tracking branch 'origin/development' into development 2024-05-06 23:42:17 -04:00
morpheus65535 5630c441b0 Added a database migration to get past the issues with incomplete table_languages_profiles. ##2485 2024-05-06 23:42:02 -04:00
dependabot[bot] d886515f9c
no log: Bump recharts from 2.12.4 to 2.12.6 in /frontend (#2487)
Bumps [recharts](https://github.com/recharts/recharts) from 2.12.4 to 2.12.6.
- [Release notes](https://github.com/recharts/recharts/releases)
- [Changelog](https://github.com/recharts/recharts/blob/3.x/CHANGELOG.md)
- [Commits](https://github.com/recharts/recharts/compare/v2.12.4...v2.12.6)

---
updated-dependencies:
- dependency-name: recharts
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 19:06:38 -04:00
Anderson Shindy Oki 970b0f9d47
Added animetosho release info 2024-05-04 13:19:36 -04:00
morpheus65535 0bddb5ba55 no log: pep8 fixes 2024-05-02 22:53:36 -04:00
morpheus65535 2c4ed03817 Fixed HI subtitles identification when downloading and improved some constants. #2386 2024-05-02 22:05:41 -04:00
JayZed bea2f0b781
Fixed embedded ASS subtitles writing encoding error
For a couple of files, I had UnicodeEncodeErrors raised when writing out a file it had successfully read in.
In my case, the output file was truncated to 1 KB.
2024-05-02 06:32:03 -04:00
Wim de With ad151ff139
Added timeout to update check API call 2024-05-01 06:13:55 -04:00
Anderson Shindy Oki 2782551c9b
Fixed Animetosho provider error for tv shows
* chore: Skip anime

* wip
2024-04-30 06:28:41 -04:00
dependabot[bot] 1c2538ef3c
no log: Bump @testing-library/react from 14.3.0 to 15.0.5 in /frontend (#2478)
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 14.3.0 to 15.0.5.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v14.3.0...v15.0.5)

---
updated-dependencies:
- dependency-name: "@testing-library/react"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 22:13:28 -04:00
JayZed 5749971d67
Improved whisper provider to not throttle when unsupported audio language is encountered. #2474
As we have noted before, bad input data should be no reason to throttle a provider.
In this case, if the input language was not supported by whisper, we were raising a ValueError that was never caught and causing an error in the whisper provider for which it was throttled.
Instead, we are now detecting this case and logging an error message.
However, given that the input language was not one of the 99 currently known to whisper, it's probably a mislabeled audio track. If the user desired output language is English, then we will tell whisper that the input audio is also English and ask it to transcribe it. Whisper does a very good job of transcribing almost anything to English, so it's worth a try.
This should address the throttling in issue #2474.
2024-04-29 22:11:47 -04:00
morpheus65535 c5a5dc9ddf no log: fixed tasks view when running in dev environment (--no-tasks). 2024-04-29 16:06:34 -04:00
Anderson Shindy Oki 86b889d3b6
Improved cutoff options label. #2466 2024-04-24 22:34:42 -04:00
Anderson Shindy Oki 0bdfcd0eda
no log: Fix anidb enrichment return type (#2472) 2024-04-24 20:57:39 -04:00
Anderson Shindy Oki 5e0433834e
Fixed animetosho provider empty subtitle name. #2468 2024-04-24 20:27:04 -04:00
morpheus65535 fd190aad14 Fixed SyntaxWarning with Python 3.12. #2462 2024-04-24 06:49:38 -04:00
Vitiko 369b2c7343 Embedded Subtitles provider: handle FileNotFoundError 2024-04-23 17:20:36 -04:00
Anderson Shindy Oki a2fee0e1e4
Fixed Anidb refinement for not anime episodes. #2463 2024-04-21 22:11:32 -04:00
morpheus65535 6dbe143364 Added minimal Python 3.12 compatibility. Not yet official support. 2024-04-21 21:31:16 -04:00
morpheus65535 516df9d410 Merge remote-tracking branch 'origin/development' into development 2024-04-20 10:50:52 -04:00
morpheus65535 03dc2109f3 no log: pep8 fix 2024-04-20 10:50:32 -04:00
morpheus65535 abc4500443 Additional fix for restart process. #2456 2024-04-20 10:47:53 -04:00
Anderson Shindy Oki 6ecfc11012
no log: add dependabot groups (#2461) 2024-04-20 08:08:06 -04:00
Anderson Shindy Oki 73224866cb
Added additional languages to animetosho provider 2024-04-19 14:18:36 -04:00
morpheus65535 a39d874d3b Fixed upgrade process to properly upgrade bazarr.py when it's updated. #2456 2024-04-18 22:34:10 -04:00
Hlib 0322f22a51
no log: add AvistaZ and CinemaZ to supported providers list in readme 2024-04-18 12:38:38 -04:00
Anderson Shindy Oki eff4568f72
no log: table overflow wrapper specific to upload modal (#2459)
* fix: table overflow wrapper specific to upload modal

* chore: apply prettier
2024-04-16 22:54:07 -04:00
Anderson Shindy Oki b7be8007f2
no log: add animetosho provider anidb integration required message (#2457)
* chore: add animetosho provider anidb intergration required message

* chore: cs
2024-04-16 20:57:55 -04:00
morpheus65535 e4bc792ee0 Fixed mass editor returning a 413 error with a large series/movies set. 2024-04-16 20:54:52 -04:00
morpheus65535 a8c352854f Fixed Swagger UI broken since last libraries update (1.4.3-beta) 2024-04-16 20:20:34 -04:00
Anderson Shindy Oki af893847c6
Fixed subtitle toolbox overlap 2024-04-15 21:24:45 -04:00
morpheus65535 7578b8ef14 Updated apprise to version 1.7.6 2024-04-15 15:00:10 -04:00
Anderson Shindy Oki b2d807d9d9
Fixed manual upload table long names without spacing. #2448 2024-04-15 11:42:15 -04:00
morpheus65535 77a157f0dd Merge remote-tracking branch 'origin/development' into development 2024-04-15 08:17:55 -04:00
morpheus65535 8037ec033f Fixed the upgrade subtitles loop when languages profile is set to normal or HI. We now delete the previous subtitles just before saving the new one. 2024-04-15 08:17:51 -04:00
Anderson Shindy Oki 77ebd036f2
Added animetosho provider 2024-04-14 08:19:13 -04:00
morpheus65535 3c30492e71 Improved best subtitles logging when score is below minimum score. 2024-04-10 23:11:00 -04:00
morpheus65535 6fc4b41526 Upgraded some frontend dependencies 2024-04-09 06:29:51 -04:00
morpheus65535 adb9f4fb37 Merge remote-tracking branch 'origin/development' into development 2024-04-08 21:51:24 -04:00
morpheus65535 cdf40950b2
no log: revert Bump @fortawesome/free-regular-svg-icons in /frontend (#2446)" (#2447)
This reverts commit d91b276496.
2024-04-08 21:50:51 -04:00
morpheus65535 b2c0db61d0 Merge remote-tracking branch 'origin/development' into development 2024-04-08 21:38:17 -04:00
dependabot[bot] d91b276496
no log: Bump @fortawesome/free-regular-svg-icons in /frontend (#2446)
Bumps [@fortawesome/free-regular-svg-icons](https://github.com/FortAwesome/Font-Awesome) from 6.5.1 to 6.5.2.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/6.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/6.5.1...6.5.2)

---
updated-dependencies:
- dependency-name: "@fortawesome/free-regular-svg-icons"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-08 20:58:37 -04:00
morpheus65535 3e9cfea3c5 Fixed restart loop when TCP port already in use. 2024-04-06 10:03:09 -04:00
dependabot[bot] 221d5aa2db
no log: Bump recharts from 2.12.2 to 2.12.3 in /frontend (#2444)
Bumps [recharts](https://github.com/recharts/recharts) from 2.12.2 to 2.12.3.
- [Release notes](https://github.com/recharts/recharts/releases)
- [Changelog](https://github.com/recharts/recharts/blob/3.x/CHANGELOG.md)
- [Commits](https://github.com/recharts/recharts/compare/v2.12.2...v2.12.3)

---
updated-dependencies:
- dependency-name: recharts
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-02 16:19:01 -04:00
morpheus65535 ad16acb88f Fixed improper redirection from login page when base_url isn't empty. 2024-04-01 10:01:09 -04:00
morpheus65535 4d11b9580c Fixed login page getting called even if authentication is disabled. #2441 2024-03-30 09:09:08 -04:00
Caden Gobat a41dc546aa
no log: Correct some misspellings and grammar (#2440) 2024-03-27 06:24:01 -04:00
dependabot[bot] 538e28fce6
no log: Bump @types/react from 18.2.65 to 18.2.70 in /frontend (#2438)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.2.65 to 18.2.70.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 21:17:49 -04:00
dependabot[bot] 22a0f3be21
no log: Bump actions/setup-node from 3 to 4 (#2437)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 21:15:27 -04:00
morpheus65535 5fc93b48fb
no log: Update schedule.yaml 2024-03-20 13:22:21 -04:00
morpheus65535 5429749e72
no log: Update schedule.yaml 2024-03-20 13:22:04 -04:00
morpheus65535 c2ed1cdb58
no log: Update schedule.yaml 2024-03-20 13:20:51 -04:00
morpheus65535 8b6204d24f
no log: Update schedule.yaml 2024-03-20 13:19:49 -04:00
morpheus65535 19bc725c1b Merge remote-tracking branch 'origin/development' into development 2024-03-18 20:28:26 -04:00
morpheus65535 b4071f0af6 Fixed betaseries provider when series doesn't exist. #2431 2024-03-18 20:28:19 -04:00
dependabot[bot] 369622834b
no log: Bump @types/node from 20.11.26 to 20.11.29 in /frontend (#2435)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.11.26 to 20.11.29.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-18 19:10:42 -04:00
dependabot[bot] 1852337ec7
no log: Bump actions/setup-python from 4 to 5 (#2434)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-18 19:09:22 -04:00
JayZed ec85f6e172
Improved multiple exceptions catching and fixed some other providers issues
* Backup files should be listed with newest ones first

Just like Sonarr and Radarr and everyone else.

* Add check_parser_binary() validation method

This is mainly to prevent the user from selecting mediainfo as the subtitles parser if it has not yet been installed on the user's system somewhere in the PATH.

* import JSONDecodeError from requests.exceptions  instead of json

Because sometimes it will return the simplejson one instead and that one won't be caught by the except clause.

* import JSONDecodeError from requests.exceptions  instead of json

Because sometimes it will return the simplejson one instead and that one won't be caught by the except clause.

Also fixed User-Agent assignment.
2024-03-16 22:10:50 -04:00
morpheus65535 1c25d125d3 Merge remote-tracking branch 'origin/development' into development 2024-03-12 23:42:53 -04:00
morpheus65535 a9f438b548 Fixed overflowing of episode titles in wanted view. #2419 2024-03-12 23:42:49 -04:00
JayZed 814b1af79f
Fixed and improved UI while correcting text 2024-03-12 23:03:08 -04:00
morpheus65535 2b9275244b Updated other React dependencies. 2024-03-12 22:48:00 -04:00
dependabot[bot] 4d94aa2675
no log: Bump @types/react-dom from 18.2.19 to 18.2.21 in /frontend (#2427)
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 18.2.19 to 18.2.21.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 20:10:39 -04:00
dependabot[bot] 5a447fe488
no log: Bump actions/cache from 3 to 4 (#2428)
Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 20:09:49 -04:00
morpheus65535 aedf2d4d89 Updated apprise to latest version to prevent deadlocks issue in 1.7.3. 2024-03-10 09:44:50 -04:00
morpheus65535 35eabb6a83 no log: updated CI dependencies 2024-03-08 13:41:24 -05:00
morpheus65535 213a04405d Rolled back cloudscraper to fix captcha v1 solving issue. 2024-03-07 21:43:26 -05:00
morpheus65535 1428edfb8b Updated fese module to latest version to fix embedded subtitles provider. #2423 2024-03-05 17:21:42 -05:00
JayZed afa529c4b3
Added Test Connection button for whisper provider 2024-03-04 23:10:50 -05:00
JayZed 345b6b3718
Fixed unbound local variable if 0 movies in database. #2417 2024-03-04 22:53:30 -05:00
JayZed 33f82fe445
Added Weekly Sync option for Radarr and Sonarr 2024-03-04 22:52:24 -05:00
morpheus65535 e5db62eb95 no log: latest apprise upgrade 2024-03-03 22:47:09 -05:00
morpheus65535 20d235e1b5 no log: small format fix 2024-03-03 22:27:00 -05:00
morpheus65535 a85ebd86aa no log: small version fix 2024-03-03 22:24:08 -05:00
morpheus65535 03afeb3470
Updated multiple Python modules (now in libs and custom_libs directories) and React libraries 2024-03-03 12:15:23 -05:00
JayZed 9ae684240b
Refactored Shutdown, Restart and exit status codes 2024-03-03 12:00:50 -05:00
JayZed c4553452a5
Allow numeric passwords for all providers. #2416 2024-03-03 11:47:30 -05:00
JayZed 5d87b10475
Fixed subtitles synchronization process when settings values have changed since Bazarr started 2024-03-01 14:43:09 -05:00
morpheus65535 af370372de Merge remote-tracking branch 'origin/development' into development 2024-02-28 22:41:47 -05:00
JayZed 9f3992d9ca no log: Prettier version? 2024-02-28 22:41:41 -05:00
JayZed 16a3158f5e Prettier version? 2024-02-28 21:47:59 -05:00
JayZed 3827ea6ffe Text updates, especially for thresholds
- added explanations for various thresholds
- fixed some spelling and grammar mistakes
- improved wording to make things clearer (hopefully)
- made capitalization, use of periods more consistent
2024-02-28 21:09:45 -05:00
Thijmen Heuvelink 731e44cb9a
Added Progressive Web App support 2024-02-28 19:43:16 -05:00
morpheus65535 6ca3689f2e no log: fixed multiple PEP8 lint issues 2024-02-27 23:51:10 -05:00
JayZed b24ee309ed Include server URL in Yify subtitle page link
Fix for issue #2409
2024-02-26 12:06:45 -05:00
JayZed f95db43a2f
Disabled autoscroll to top for underlying window after manual search. #2285 2024-02-26 06:33:06 -05:00
JayZed f71b8931e3
Fixed subtitles sync function to preserve subtitles file extension. #2399 2024-02-26 06:28:51 -05:00
JayZed 6ba720969e
Improved debug logging for whisperai 2024-02-26 06:25:10 -05:00
github-actions[bot] 56d54e405b
Merge development into master 2024-02-20 00:29:19 +00:00
github-actions[bot] 38094e6323
Merge development into master 2024-02-04 01:30:27 +00:00
morpheus65535 8282899fac Merge branch 'development'
# Conflicts:
#	.github/workflows/ci.yml
2023-11-28 07:28:57 -05:00
github-actions[bot] a09cc34e09
Merge development into master 2023-10-14 12:45:55 +00:00
github-actions[bot] 823f3d8d3f
Merge development into master 2023-09-16 02:44:25 +00:00
Liang Yi 07697fa212
no log: Revert previous commits from dependbot 2023-08-06 19:11:45 +00:00
dependabot[bot] 643c120c91
no log: Bump vite from 4.3.2 to 4.3.9 in /frontend (#2182)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.3.2 to 4.3.9.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.3.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-07 02:55:00 +08:00
dependabot[bot] 1834fbacd9
no log: Bump tough-cookie from 4.1.2 to 4.1.3 in /frontend (#2198)
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-07 02:54:43 +08:00
dependabot[bot] 54248ac592
no log: Bump word-wrap from 1.2.3 to 1.2.4 in /frontend (#2204)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-07 02:54:26 +08:00
morpheus65535 ec2d10f195 no log: fix CI 2023-08-04 10:43:10 -04:00
github-actions[bot] 64af56cb80
Merge development into master 2023-07-22 13:49:00 +00:00
github-actions[bot] 3c2f940469
Merge development into master 2023-07-11 00:28:02 +00:00
morpheus65535 77f3ff82d5 no log: fix changelog template 2023-06-24 18:19:57 -04:00
morpheus65535 080710e7e1 Merge branch 'development'
# Conflicts:
#	frontend/package-lock.json
#	frontend/package.json
2023-06-24 18:17:40 -04:00
Liang Yi 38d95c5a7c
no log: Revert "no log: Bump socket.io-parser from 4.2.2 to 4.2.3 in /frontend (#2150)"
This reverts commit e7ce635a86.
2023-05-26 02:52:24 +00:00
dependabot[bot] e7ce635a86
no log: Bump socket.io-parser from 4.2.2 to 4.2.3 in /frontend (#2150)
Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 4.2.2 to 4.2.3.
- [Release notes](https://github.com/socketio/socket.io-parser/releases)
- [Changelog](https://github.com/socketio/socket.io-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-parser/compare/4.2.2...4.2.3)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-25 23:22:56 +08:00
morpheus65535 b485dd9c71 Merge branch 'development'
# Conflicts:
#	frontend/package-lock.json
2023-05-01 20:39:33 -04:00
dependabot[bot] 1fa4cf6afc
no log: Bump d3-color and recharts in /frontend (#2079)
Bumps [d3-color](https://github.com/d3/d3-color) to 3.1.0 and updates ancestor dependency [recharts](https://github.com/recharts/recharts). These dependencies need to be updated together.


Updates `d3-color` from 2.0.0 to 3.1.0
- [Release notes](https://github.com/d3/d3-color/releases)
- [Commits](https://github.com/d3/d3-color/compare/v2.0.0...v3.1.0)

Updates `recharts` from 2.1.16 to 2.4.3
- [Release notes](https://github.com/recharts/recharts/releases)
- [Changelog](https://github.com/recharts/recharts/blob/master/CHANGELOG.md)
- [Commits](https://github.com/recharts/recharts/compare/v2.1.16...v2.4.3)

---
updated-dependencies:
- dependency-name: d3-color
  dependency-type: indirect
- dependency-name: recharts
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 00:53:45 +08:00
github-actions[bot] 71a2c758b7
Merge development into master 2023-03-03 02:12:52 +00:00
morpheus65535 1836014ad3
Delete announcements.json 2023-02-12 08:10:39 -05:00
morpheus65535 f3507c4d63
Create announcements.json 2023-02-11 09:42:46 -05:00
github-actions[bot] 0c7e422297
Merge development into master 2022-12-31 16:37:03 +00:00
github-actions[bot] 5722085d1e
Merge development into master 2022-12-05 02:33:36 +00:00
github-actions[bot] 70346950fd
Merge development into master 2022-10-15 12:45:09 +00:00
github-actions[bot] 5882fc07d2
Merge development into master 2022-08-31 02:43:49 +00:00
github-actions[bot] e439f2e3ed
Merge development into master 2022-07-02 12:48:11 +00:00
morpheus65535 135bdf2d45 Merge branch 'development'
# Conflicts:
#	frontend/package-lock.json
2022-04-30 09:09:50 -04:00
github-actions[bot] 1a45fa67bc
Merge development into master 2022-02-26 15:03:54 +00:00
dependabot[bot] bd1423891c
no log: Bump nanoid from 3.1.23 to 3.3.1 in /frontend (#1736)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.23 to 3.3.1.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.23...3.3.1)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-22 22:59:41 -05:00
dependabot[bot] 2a740fb26d
no log: Bump url-parse from 1.5.3 to 1.5.7 in /frontend (#1729)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.3 to 1.5.7.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.3...1.5.7)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-22 22:56:23 -05:00
github-actions[bot] e305aad597
Merge development into master 2021-12-30 11:52:19 +00:00
github-actions[bot] e1f836dfea
Merge development into master 2021-11-19 01:45:58 +00:00
github-actions[bot] 88b69b5243
Merge development into master 2021-10-12 23:45:07 +00:00
github-actions[bot] ac2052f43d
Merge development into master 2021-09-11 12:47:42 +00:00
github-actions[bot] c66d5662b4
Merge development into master 2021-08-31 16:54:28 +00:00
github-actions[bot] 87bc9ecd29
Merge development into master 2021-08-13 12:06:07 +00:00
github-actions[bot] 80ffdc91b7
Merge development into master 2021-07-19 01:29:47 +00:00
morpheus65535 15d32b61df no log: test action to make sure that Bazarr is starting properly 2021-06-19 10:10:47 -04:00
morpheus65535 5af382e62d no log: test action to make sure that Bazarr is starting properly 2021-06-19 10:03:00 -04:00
morpheus65535 da8d13ff78 no log: test action to make sure that Bazarr is starting properly 2021-06-19 09:48:44 -04:00
Liang Yi 3af4c39e73
no log: Revert wrong commit 2021-05-09 15:27:43 +08:00
Liang Yi 23cf9d4c13
no log: Fix pipeline 2021-05-09 15:26:12 +08:00
github-actions[bot] 2bceafe5af
Merge development into master 2021-05-08 14:06:43 +00:00
github-actions[bot] 1e059d0cee
Merge development into master 2021-04-19 13:27:31 +00:00
3613 changed files with 204637 additions and 119931 deletions

View File

@ -8,6 +8,19 @@ updates:
prefix: "[bot]"
open-pull-requests-limit: 1
target-branch: "development"
groups:
fortawesome:
patterns:
- "@fortawesome*"
mantine:
patterns:
- "@mantine*"
react:
patterns:
- "react"
- "react-dom"
- "@types/react"
- "@types/react-dom"
- package-ecosystem: 'github-actions'
directory: '/'
schedule:

View File

@ -1,4 +1,5 @@
bazarr
custom_libs
frontend/build
libs
bazarr.py

View File

@ -27,14 +27,14 @@ jobs:
uses: actions/checkout@v4
- name: Cache node_modules
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: "${{ env.UI_DIRECTORY }}/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-modules-
- name: Setup NodeJS
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: "lts/*"
@ -62,7 +62,7 @@ jobs:
run: npm run build:ci
working-directory: ${{ env.UI_DIRECTORY }}
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: ${{ env.UI_ARTIFACT_NAME }}
path: "${{ env.UI_DIRECTORY }}/build"
@ -76,12 +76,12 @@ jobs:
uses: actions/checkout@v4
- name: Set up Python 3.8
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.8"
- name: Install UI
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: ${{ env.UI_ARTIFACT_NAME }}
path: "${{ env.UI_DIRECTORY }}/build"

View File

@ -29,14 +29,14 @@ jobs:
git fetch --depth ${{ env.FETCH_DEPTH }} --tags
- name: Cache node_modules
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: "${{ env.UI_DIRECTORY }}/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-modules-
- name: Setup NodeJS
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: "lts/*"

View File

@ -31,14 +31,14 @@ jobs:
run: git config --global user.name "github-actions"
- name: Cache node_modules
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: "${{ env.UI_DIRECTORY }}/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-modules-
- name: Setup NodeJS
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: "lts/*"

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Execute
uses: benc-uk/workflow-dispatch@v121
uses: benc-uk/workflow-dispatch@v1.2.3
with:
workflow: "release_beta_to_dev"
token: ${{ secrets.WF_GITHUB_TOKEN }}

View File

@ -22,7 +22,7 @@ jobs:
ref: development
- name: Setup NodeJS
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: "lts/*"
@ -35,7 +35,7 @@ jobs:
working-directory: ${{ env.UI_DIRECTORY }}
- name: Set up Python 3.8
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.8"

View File

@ -48,7 +48,9 @@ If you need something that is not already part of Bazarr, feel free to create a
## Supported subtitles providers:
- Addic7ed
- Animetosho (requires AniDb HTTP API client described [here](https://wiki.anidb.net/HTTP_API_Definition))
- Assrt
- AvistaZ, CinemaZ (Get session cookies using method described [here](https://github.com/morpheus65535/bazarr/pull/2375#issuecomment-2057010996))
- BetaSeries
- BSplayer
- Embedded Subtitles

116
bazarr.py
View File

@ -6,9 +6,14 @@ import signal
import subprocess
import sys
import time
import atexit
from bazarr.app.get_args import args
from bazarr.literals import EXIT_PYTHON_UPGRADE_NEEDED, EXIT_NORMAL, FILE_RESTART, FILE_STOP, ENV_RESTARTFILE, ENV_STOPFILE, EXIT_INTERRUPT
def exit_program(status_code):
print(f'Bazarr exited with status code {status_code}.')
raise SystemExit(status_code)
def check_python_version():
@ -19,7 +24,7 @@ def check_python_version():
if int(python_version[0]) < minimum_py3_tuple[0]:
print("Python " + minimum_py3_str + " or greater required. "
"Current version is " + platform.python_version() + ". Please upgrade Python.")
sys.exit(1)
exit_program(EXIT_PYTHON_UPGRADE_NEEDED)
elif int(python_version[0]) == 3 and int(python_version[1]) > 11:
print("Python version greater than 3.11.x is unsupported. Current version is " + platform.python_version() +
". Keep in mind that even if it works, you're on your own.")
@ -27,7 +32,7 @@ def check_python_version():
(int(python_version[0]) != minimum_py3_tuple[0]):
print("Python " + minimum_py3_str + " or greater required. "
"Current version is " + platform.python_version() + ". Please upgrade Python.")
sys.exit(1)
exit_program(EXIT_PYTHON_UPGRADE_NEEDED)
def get_python_path():
@ -50,79 +55,98 @@ check_python_version()
dir_name = os.path.dirname(__file__)
def end_child_process(ep):
try:
if os.name != 'nt':
try:
ep.send_signal(signal.SIGINT)
except ProcessLookupError:
pass
else:
import win32api
import win32con
try:
win32api.GenerateConsoleCtrlEvent(win32con.CTRL_C_EVENT, ep.pid)
except KeyboardInterrupt:
pass
except:
ep.terminate()
def start_bazarr():
script = [get_python_path(), "-u", os.path.normcase(os.path.join(dir_name, 'bazarr', 'main.py'))] + sys.argv[1:]
ep = subprocess.Popen(script, stdout=None, stderr=None, stdin=subprocess.DEVNULL)
atexit.register(end_child_process, ep=ep)
signal.signal(signal.SIGTERM, lambda signal_no, frame: end_child_process(ep))
ep = subprocess.Popen(script, stdout=None, stderr=None, stdin=subprocess.DEVNULL, env=os.environ)
print(f"Bazarr starting child process with PID {ep.pid}...")
return ep
def terminate_child():
print(f"Terminating child process with PID {child_process.pid}")
child_process.terminate()
def get_stop_status_code(input_file):
try:
with open(input_file, 'r') as file:
# read status code from file, if it exists
line = file.readline()
try:
status_code = int(line)
except (ValueError, TypeError):
status_code = EXIT_NORMAL
file.close()
except Exception:
status_code = EXIT_NORMAL
return status_code
def check_status():
if os.path.exists(stopfile):
global child_process
if os.path.exists(stop_file):
status_code = get_stop_status_code(stop_file)
try:
os.remove(stopfile)
print("Deleting stop file...")
os.remove(stop_file)
except Exception:
print('Unable to delete stop file.')
finally:
print('Bazarr exited.')
sys.exit(0)
terminate_child()
exit_program(status_code)
if os.path.exists(restartfile):
if os.path.exists(restart_file):
try:
os.remove(restartfile)
print("Deleting restart file...")
os.remove(restart_file)
except Exception:
print('Unable to delete restart file.')
else:
finally:
terminate_child()
print("Bazarr is restarting...")
start_bazarr()
child_process = start_bazarr()
def interrupt_handler(signum, frame):
# catch and ignore keyboard interrupt Ctrl-C
# the child process Server object will catch SIGINT and perform an orderly shutdown
global interrupted
if not interrupted:
# ignore user hammering Ctrl-C; we heard you the first time!
interrupted = True
print('Handling keyboard interrupt...')
else:
print("Stop doing that! I heard you the first time!")
if __name__ == '__main__':
restartfile = os.path.join(args.config_dir, 'bazarr.restart')
stopfile = os.path.join(args.config_dir, 'bazarr.stop')
interrupted = False
signal.signal(signal.SIGINT, interrupt_handler)
restart_file = os.path.join(args.config_dir, FILE_RESTART)
stop_file = os.path.join(args.config_dir, FILE_STOP)
os.environ[ENV_STOPFILE] = stop_file
os.environ[ENV_RESTARTFILE] = restart_file
# Cleanup leftover files
try:
os.remove(restartfile)
os.remove(restart_file)
except FileNotFoundError:
pass
try:
os.remove(stopfile)
os.remove(stop_file)
except FileNotFoundError:
pass
# Initial start of main bazarr process
print("Bazarr starting...")
start_bazarr()
child_process = start_bazarr()
# Keep the script running forever until stop is requested through term or keyboard interrupt
# Keep the script running forever until stop is requested through term, special files or keyboard interrupt
while True:
check_status()
try:
if sys.platform.startswith('win'):
time.sleep(5)
else:
os.wait()
time.sleep(1)
time.sleep(5)
except (KeyboardInterrupt, SystemExit, ChildProcessError):
print('Bazarr exited.')
sys.exit(0)
# this code should never be reached, if signal handling is working properly
print('Bazarr exited main script file via keyboard interrupt.')
exit_program(EXIT_INTERRUPT)

View File

@ -1,14 +1,14 @@
# coding=utf-8
import io
import os
import re
from flask_restx import Resource, Namespace, fields, marshal
from app.config import settings
from app.logger import empty_log
from app.get_args import args
from utilities.central import get_log_file_path
from ..utils import authenticate
api_ns_system_logs = Namespace('System Logs', description='List log file entries or empty log file')
@ -43,18 +43,18 @@ class SystemLogs(Resource):
if len(include) > 0:
try:
include_compiled = re.compile(include, flags)
except:
except Exception:
include_compiled = None
if len(exclude) > 0:
try:
exclude_compiled = re.compile(exclude, flags)
except:
except Exception:
exclude_compiled = None
elif ignore_case:
include = include.casefold()
exclude = exclude.casefold()
with io.open(os.path.join(args.config_dir, 'log', 'bazarr.log'), encoding='UTF-8') as file:
with io.open(get_log_file_path(), encoding='UTF-8') as file:
raw_lines = file.read()
lines = raw_lines.split('|\n')
for line in lines:

View File

@ -1,6 +1,6 @@
# coding=utf-8
from flask import Flask, redirect
from flask import Flask, redirect, Request
from flask_compress import Compress
from flask_cors import CORS
@ -13,9 +13,17 @@ from .config import settings, base_url
socketio = SocketIO()
class CustomRequest(Request):
def __init__(self, *args, **kwargs):
super(CustomRequest, self).__init__(*args, **kwargs)
# required to increase form-data size before returning a 413
self.max_form_parts = 10000
def create_app():
# Flask Setup
app = Flask(__name__)
app.request_class = CustomRequest
app.config['COMPRESS_ALGORITHM'] = 'gzip'
Compress(app)
app.wsgi_app = ReverseProxied(app.wsgi_app)
@ -34,6 +42,7 @@ def create_app():
else:
app.config["DEBUG"] = False
from engineio.async_drivers import threading # noqa W0611 # required to prevent an import exception in engineio
socketio.init_app(app, path=f'{base_url.rstrip("/")}/api/socket.io', cors_allowed_origins='*',
async_mode='threading', allow_upgrades=False, transports='polling', engineio_logger=False)

View File

@ -25,7 +25,7 @@ def check_releases():
url_releases = 'https://api.github.com/repos/morpheus65535/Bazarr/releases?per_page=100'
try:
logging.debug(f'BAZARR getting releases from Github: {url_releases}')
r = requests.get(url_releases, allow_redirects=True)
r = requests.get(url_releases, allow_redirects=True, timeout=15)
r.raise_for_status()
except requests.exceptions.HTTPError:
logging.exception("Error trying to get releases from Github. Http error.")
@ -160,12 +160,14 @@ def apply_update():
'BAZARR was unable to delete the previous build directory during upgrade process.')
for file in archive.namelist():
if file.startswith(zip_root_directory) and file != zip_root_directory and not \
file.endswith('bazarr.py'):
if file.startswith(zip_root_directory) and file != zip_root_directory:
file_path = os.path.join(bazarr_dir, file[len(zip_root_directory):])
parent_dir = os.path.dirname(file_path)
os.makedirs(parent_dir, exist_ok=True)
if not os.path.isdir(file_path):
if os.path.exists(file_path):
# remove the file first to handle case-insensitive file systems
os.remove(file_path)
with open(file_path, 'wb+') as f:
f.write(archive.read(file))
except Exception:
@ -230,6 +232,9 @@ def update_cleaner(zipfile, bazarr_dir, config_dir):
dir_to_ignore_regex = re.compile(dir_to_ignore_regex_string)
file_to_ignore = ['nssm.exe', '7za.exe', 'unins000.exe', 'unins000.dat']
# prevent deletion of leftover Apprise.py/pyi files after 1.8.0 version that caused issue on case-insensitive
# filesystem. This could be removed in a couple of major versions.
file_to_ignore += ['Apprise.py', 'Apprise.pyi', 'apprise.py', 'apprise.pyi']
logging.debug(f'BAZARR upgrade leftover cleaner will ignore those files: {", ".join(file_to_ignore)}')
extension_to_ignore = ['.pyc']
logging.debug(

View File

@ -7,6 +7,9 @@ import logging
import re
from urllib.parse import quote_plus
from utilities.binaries import BinaryNotFound, get_binary
from literals import EXIT_VALIDATION_ERROR
from utilities.central import stop_bazarr
from subliminal.cache import region
from dynaconf import Dynaconf, Validator as OriginalValidator
from dynaconf.loaders.yaml_loader import write
@ -38,6 +41,7 @@ def validate_ip_address(ip_string):
ONE_HUNDRED_YEARS_IN_MINUTES = 52560000
ONE_HUNDRED_YEARS_IN_HOURS = 876000
class Validator(OriginalValidator):
# Give the ability to personalize messages sent by the original dynasync Validator class.
default_messages = MappingProxyType(
@ -51,6 +55,14 @@ class Validator(OriginalValidator):
)
def check_parser_binary(value):
try:
get_binary(value)
except BinaryNotFound:
raise ValidationError(f"Executable '{value}' not found in search path. Please install before making this selection.")
return True
validators = [
# general section
Validator('general.flask_secret_key', must_exist=True, default=hexlify(os.urandom(16)).decode(),
@ -97,13 +109,14 @@ validators = [
Validator('general.adaptive_searching_delta', must_exist=True, default='1w', is_type_of=str,
is_in=['3d', '1w', '2w', '3w', '4w']),
Validator('general.enabled_providers', must_exist=True, default=[], is_type_of=list),
Validator('general.enabled_integrations', must_exist=True, default=[], is_type_of=list),
Validator('general.multithreading', must_exist=True, default=True, is_type_of=bool),
Validator('general.chmod_enabled', must_exist=True, default=False, is_type_of=bool),
Validator('general.chmod', must_exist=True, default='0640', is_type_of=str),
Validator('general.subfolder', must_exist=True, default='current', is_type_of=str),
Validator('general.subfolder_custom', must_exist=True, default='', is_type_of=str),
Validator('general.upgrade_subs', must_exist=True, default=True, is_type_of=bool),
Validator('general.upgrade_frequency', must_exist=True, default=12, is_type_of=int,
Validator('general.upgrade_frequency', must_exist=True, default=12, is_type_of=int,
is_in=[6, 12, 24, ONE_HUNDRED_YEARS_IN_HOURS]),
Validator('general.days_to_upgrade_subs', must_exist=True, default=7, is_type_of=int, gte=0, lte=30),
Validator('general.upgrade_manual', must_exist=True, default=True, is_type_of=bool),
@ -116,7 +129,7 @@ validators = [
Validator('general.dont_notify_manual_actions', must_exist=True, default=False, is_type_of=bool),
Validator('general.hi_extension', must_exist=True, default='hi', is_type_of=str, is_in=['hi', 'cc', 'sdh']),
Validator('general.embedded_subtitles_parser', must_exist=True, default='ffprobe', is_type_of=str,
is_in=['ffprobe', 'mediainfo']),
is_in=['ffprobe', 'mediainfo'], condition=check_parser_binary),
Validator('general.default_und_audio_lang', must_exist=True, default='', is_type_of=str),
Validator('general.default_und_embedded_subtitles_lang', must_exist=True, default='', is_type_of=str),
Validator('general.parse_embedded_audio_track', must_exist=True, default=False, is_type_of=bool),
@ -162,7 +175,7 @@ validators = [
Validator('sonarr.full_update_hour', must_exist=True, default=4, is_type_of=int, gte=0, lte=23),
Validator('sonarr.only_monitored', must_exist=True, default=False, is_type_of=bool),
Validator('sonarr.series_sync', must_exist=True, default=60, is_type_of=int,
is_in=[15, 60, 180, 360, 720, 1440, ONE_HUNDRED_YEARS_IN_MINUTES]),
is_in=[15, 60, 180, 360, 720, 1440, 10080, ONE_HUNDRED_YEARS_IN_MINUTES]),
Validator('sonarr.excluded_tags', must_exist=True, default=[], is_type_of=list),
Validator('sonarr.excluded_series_types', must_exist=True, default=[], is_type_of=list),
Validator('sonarr.use_ffprobe_cache', must_exist=True, default=True, is_type_of=bool),
@ -185,7 +198,7 @@ validators = [
Validator('radarr.full_update_hour', must_exist=True, default=4, is_type_of=int, gte=0, lte=23),
Validator('radarr.only_monitored', must_exist=True, default=False, is_type_of=bool),
Validator('radarr.movies_sync', must_exist=True, default=60, is_type_of=int,
is_in=[15, 60, 180, 360, 720, 1440, ONE_HUNDRED_YEARS_IN_MINUTES]),
is_in=[15, 60, 180, 360, 720, 1440, 10080, ONE_HUNDRED_YEARS_IN_MINUTES]),
Validator('radarr.excluded_tags', must_exist=True, default=[], is_type_of=list),
Validator('radarr.use_ffprobe_cache', must_exist=True, default=True, is_type_of=bool),
Validator('radarr.defer_search_signalr', must_exist=True, default=False, is_type_of=bool),
@ -222,6 +235,11 @@ validators = [
Validator('addic7ed.user_agent', must_exist=True, default='', is_type_of=str),
Validator('addic7ed.vip', must_exist=True, default=False, is_type_of=bool),
# animetosho section
Validator('animetosho.search_threshold', must_exist=True, default=6, is_type_of=int, gte=1, lte=15),
Validator('animetosho.anidb_api_client', must_exist=True, default='', is_type_of=str, cast=str),
Validator('animetosho.anidb_api_client_ver', must_exist=True, default=1, is_type_of=int, gte=1, lte=9),
# avistaz section
Validator('avistaz.cookies', must_exist=True, default='', is_type_of=str),
Validator('avistaz.user_agent', must_exist=True, default='', is_type_of=str),
@ -275,10 +293,6 @@ validators = [
Validator('napisy24.username', must_exist=True, default='', is_type_of=str, cast=str),
Validator('napisy24.password', must_exist=True, default='', is_type_of=str, cast=str),
# subscene section
Validator('subscene.username', must_exist=True, default='', is_type_of=str, cast=str),
Validator('subscene.password', must_exist=True, default='', is_type_of=str, cast=str),
# betaseries section
Validator('betaseries.token', must_exist=True, default='', is_type_of=str, cast=str),
@ -357,6 +371,10 @@ validators = [
Validator('postgresql.database', must_exist=True, default='', is_type_of=str),
Validator('postgresql.username', must_exist=True, default='', is_type_of=str, cast=str),
Validator('postgresql.password', must_exist=True, default='', is_type_of=str, cast=str),
# anidb section
Validator('anidb.api_client', must_exist=True, default='', is_type_of=str),
Validator('anidb.api_client_ver', must_exist=True, default=1, is_type_of=int),
]
@ -409,8 +427,9 @@ while failed_validator:
settings[current_validator_details.names[0]] = current_validator_details.default
else:
logging.critical(f"Value for {current_validator_details.names[0]} doesn't pass validation and there's no "
f"default value. This issue must be reported. Bazarr won't works until it's been fixed.")
os._exit(0)
f"default value. This issue must be reported to and fixed by the development team. "
f"Bazarr won't work until it's been fixed.")
stop_bazarr(EXIT_VALIDATION_ERROR)
def write_config():
@ -429,6 +448,7 @@ array_keys = ['excluded_tags',
'subzero_mods',
'excluded_series_types',
'enabled_providers',
'enabled_integrations',
'path_mappings',
'path_mappings_movie',
'language_equals',
@ -437,7 +457,7 @@ array_keys = ['excluded_tags',
empty_values = ['', 'None', 'null', 'undefined', None, []]
str_keys = ['chmod', 'log_include_filter', 'log_exclude_filter']
str_keys = ['chmod', 'log_include_filter', 'log_exclude_filter', 'password', 'f_password', 'hashed_password']
# Increase Sonarr and Radarr sync interval since we now use SignalR feed to update in real time
if settings.sonarr.series_sync < 15:
@ -484,25 +504,27 @@ def get_settings():
settings_to_return[k].update({subk: subv})
return settings_to_return
def validate_log_regex():
# handle bug in dynaconf that changes strings to numbers, so change them back to str
if not isinstance(settings.log.include_filter, str):
settings.log.include_filter = str(settings.log.include_filter)
settings.log.include_filter = str(settings.log.include_filter)
if not isinstance(settings.log.exclude_filter, str):
settings.log.exclude_filter = str(settings.log.exclude_filter)
settings.log.exclude_filter = str(settings.log.exclude_filter)
if (settings.log.use_regex):
if settings.log.use_regex:
# compile any regular expressions specified to see if they are valid
# if invalid, tell the user which one
try:
re.compile(settings.log.include_filter)
except:
except Exception:
raise ValidationError(f"Include filter: invalid regular expression: {settings.log.include_filter}")
try:
re.compile(settings.log.exclude_filter)
except:
except Exception:
raise ValidationError(f"Exclude filter: invalid regular expression: {settings.log.exclude_filter}")
def save_settings(settings_items):
configure_debug = False
configure_captcha = False
@ -519,8 +541,7 @@ def save_settings(settings_items):
undefined_subtitles_track_default_changed = False
audio_tracks_parsing_changed = False
reset_providers = False
check_log_regex = False
# Subzero Mods
update_subzero = False
subzero_mods = get_array_from(settings.general.subzero_mods)
@ -661,15 +682,6 @@ def save_settings(settings_items):
reset_providers = True
region.delete('oscom_token')
if key == 'settings-subscene-username':
if key != settings.subscene.username:
reset_providers = True
region.delete('subscene_cookies2')
elif key == 'settings-subscene-password':
if key != settings.subscene.password:
reset_providers = True
region.delete('subscene_cookies2')
if key == 'settings-titlovi-username':
if key != settings.titlovi.username:
reset_providers = True

View File

@ -125,7 +125,7 @@ def provider_throttle_map():
PROVIDERS_FORCED_OFF = ["addic7ed", "tvsubtitles", "legendasdivx", "napiprojekt", "shooter",
"hosszupuska", "supersubtitles", "titlovi", "assrt", "subscene"]
"hosszupuska", "supersubtitles", "titlovi", "assrt"]
throttle_count = {}
@ -259,11 +259,6 @@ def get_providers_auth():
'also_foreign': False, # fixme
'verify_ssl': settings.podnapisi.verify_ssl
},
'subscene': {
'username': settings.subscene.username,
'password': settings.subscene.password,
'only_foreign': False, # fixme
},
'legendasdivx': {
'username': settings.legendasdivx.username,
'password': settings.legendasdivx.password,
@ -323,7 +318,10 @@ def get_providers_auth():
'response': settings.whisperai.response,
'timeout': settings.whisperai.timeout,
'ffmpeg_path': _FFMPEG_BINARY,
'loglevel': settings.whisperai.loglevel,
'loglevel': settings.whisperai.loglevel,
},
"animetosho": {
'search_threshold': settings.animetosho.search_threshold,
}
}
@ -498,7 +496,7 @@ def get_throttled_providers():
except Exception:
# set empty content in throttled_providers.dat
logging.error("Invalid content in throttled_providers.dat. Resetting")
set_throttled_providers(providers)
set_throttled_providers(str(providers))
finally:
return providers

View File

@ -18,6 +18,7 @@ def clean_libs():
def set_libs():
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), '../custom_libs/'))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), '../libs/'))

View File

@ -8,9 +8,9 @@ import platform
import warnings
from logging.handlers import TimedRotatingFileHandler
from utilities.central import get_log_file_path
from pytz_deprecation_shim import PytzUsageWarning
from .get_args import args
from .config import settings
@ -55,34 +55,37 @@ class NoExceptionFormatter(logging.Formatter):
def formatException(self, record):
return ''
class UnwantedWaitressMessageFilter(logging.Filter):
def filter(self, record):
if settings.general.debug == True:
if settings.general.debug:
# no filtering in debug mode
return True
unwantedMessages = [
"Exception while serving /api/socket.io/",
['Session is disconnected', 'Session not found' ],
"Exception while serving /api/socket.io/",
["'Session is disconnected'", "'Session not found'" ],
"Exception while serving /api/socket.io/",
['"Session is disconnected"', '"Session not found"' ]
unwantedMessages = [
"Exception while serving /api/socket.io/",
['Session is disconnected', 'Session not found'],
"Exception while serving /api/socket.io/",
["'Session is disconnected'", "'Session not found'"],
"Exception while serving /api/socket.io/",
['"Session is disconnected"', '"Session not found"'],
"Exception when servicing %r",
[],
]
wanted = True
wanted = True
listLength = len(unwantedMessages)
for i in range(0, listLength, 2):
if record.msg == unwantedMessages[i]:
exceptionTuple = record.exc_info
if exceptionTuple != None:
if str(exceptionTuple[1]) in unwantedMessages[i+1]:
if exceptionTuple is not None:
if len(unwantedMessages[i+1]) == 0 or str(exceptionTuple[1]) in unwantedMessages[i+1]:
wanted = False
break
return wanted
@ -91,10 +94,10 @@ def configure_logging(debug=False):
warnings.simplefilter('ignore', category=PytzUsageWarning)
# warnings.simplefilter('ignore', category=SAWarning)
if not debug:
log_level = "INFO"
if debug:
log_level = logging.DEBUG
else:
log_level = "DEBUG"
log_level = logging.INFO
logger.handlers = []
@ -106,21 +109,21 @@ def configure_logging(debug=False):
'%(asctime)-15s - %(name)-32s (%(thread)x) : %(levelname)s (%(module)s:%(lineno)d) - %(message)s')
ch.setFormatter(cf)
ch.setLevel(log_level)
ch.setLevel(logging.DEBUG)
logger.addHandler(ch)
# File Logging
global fh
if sys.version_info >= (3, 9):
fh = PatchedTimedRotatingFileHandler(os.path.join(args.config_dir, 'log/bazarr.log'), when="midnight",
fh = PatchedTimedRotatingFileHandler(get_log_file_path(), when="midnight",
interval=1, backupCount=7, delay=True, encoding='utf-8')
else:
fh = TimedRotatingFileHandler(os.path.join(args.config_dir, 'log/bazarr.log'), when="midnight", interval=1,
fh = TimedRotatingFileHandler(get_log_file_path(), when="midnight", interval=1,
backupCount=7, delay=True, encoding='utf-8')
f = FileHandlerFormatter('%(asctime)s|%(levelname)-8s|%(name)-32s|%(message)s|',
'%Y-%m-%d %H:%M:%S')
fh.setFormatter(f)
fh.setLevel(log_level)
fh.setLevel(logging.DEBUG)
logger.addHandler(fh)
if debug:
@ -159,7 +162,7 @@ def configure_logging(debug=False):
logging.getLogger("ga4mp.ga4mp").setLevel(logging.ERROR)
logging.getLogger("waitress").setLevel(logging.ERROR)
logging.getLogger("waitress").addFilter(UnwantedWaitressMessageFilter())
logging.getLogger("waitress").addFilter(UnwantedWaitressMessageFilter())
logging.getLogger("knowit").setLevel(logging.CRITICAL)
logging.getLogger("enzyme").setLevel(logging.CRITICAL)
logging.getLogger("guessit").setLevel(logging.WARNING)

View File

@ -1,6 +1,6 @@
# coding=utf-8
import apprise
from apprise import Apprise, AppriseAsset
import logging
from .database import TableSettingsNotifier, TableEpisodes, TableShows, TableMovies, database, insert, delete, select
@ -8,7 +8,7 @@ from .database import TableSettingsNotifier, TableEpisodes, TableShows, TableMov
def update_notifier():
# define apprise object
a = apprise.Apprise()
a = Apprise()
# Retrieve all the details
results = a.details()
@ -70,9 +70,9 @@ def send_notifications(sonarr_series_id, sonarr_episode_id, message):
if not episode:
return
asset = apprise.AppriseAsset(async_mode=False)
asset = AppriseAsset(async_mode=False)
apobj = apprise.Apprise(asset=asset)
apobj = Apprise(asset=asset)
for provider in providers:
if provider.url is not None:
@ -101,9 +101,9 @@ def send_notifications_movie(radarr_id, message):
else:
movie_year = ''
asset = apprise.AppriseAsset(async_mode=False)
asset = AppriseAsset(async_mode=False)
apobj = apprise.Apprise(asset=asset)
apobj = Apprise(asset=asset)
for provider in providers:
if provider.url is not None:

View File

@ -8,12 +8,14 @@ from apscheduler.triggers.interval import IntervalTrigger
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from apscheduler.events import EVENT_JOB_SUBMITTED, EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
from apscheduler.jobstores.base import JobLookupError
from datetime import datetime, timedelta
from calendar import day_name
from random import randrange
from tzlocal import get_localzone
from tzlocal.utils import ZoneInfoNotFoundError
try:
import zoneinfo # pragma: no cover
except ImportError:
from backports import zoneinfo # pragma: no cover
from dateutil import tz
import logging
@ -40,17 +42,24 @@ from dateutil.relativedelta import relativedelta
NO_INTERVAL = "None"
NEVER_DATE = "Never"
ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365
ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365
def a_long_time_from_now(job):
# job isn't scheduled at all
if job.next_run_time is None:
return True
# currently defined as more than a year from now
delta = job.next_run_time - datetime.now(job.next_run_time.tzinfo)
return delta.total_seconds() > ONE_YEAR_IN_SECONDS
def in_a_century():
century = datetime.now() + relativedelta(years=100)
return century.year
class Scheduler:
def __init__(self):
@ -58,7 +67,7 @@ class Scheduler:
try:
self.timezone = get_localzone()
except ZoneInfoNotFoundError as e:
except zoneinfo.ZoneInfoNotFoundError as e:
logging.error(f"BAZARR cannot use specified timezone: {e}")
self.timezone = tz.gettz("UTC")
@ -133,7 +142,6 @@ class Scheduler:
return ", ".join(strings)
def get_time_from_cron(cron):
year = str(cron[0])
day = str(cron[4])
hour = str(cron[5])
@ -183,8 +191,8 @@ class Scheduler:
else:
interval = get_time_from_cron(job.trigger.fields)
task_list.append({'name': job.name, 'interval': interval,
'next_run_in': next_run, 'next_run_time': next_run, 'job_id': job.id,
'job_running': running})
'next_run_in': next_run, 'next_run_time': next_run, 'job_id': job.id,
'job_running': running})
return task_list
@ -219,8 +227,8 @@ class Scheduler:
elif backup == "Manually":
trigger = CronTrigger(year=in_a_century())
self.aps_scheduler.add_job(backup_to_zip, trigger,
max_instances=1, coalesce=True, misfire_grace_time=15, id='backup',
name='Backup Database and Configuration File', replace_existing=True)
max_instances=1, coalesce=True, misfire_grace_time=15, id='backup',
name='Backup Database and Configuration File', replace_existing=True)
def __sonarr_full_update_task(self):
if settings.general.use_sonarr:
@ -316,8 +324,8 @@ class Scheduler:
self.aps_scheduler.modify_job(job.id,
next_run_time=datetime.now(tz=self.timezone) +
timedelta(seconds=randrange(
job.trigger.interval.total_seconds() * 0.75,
job.trigger.interval.total_seconds())))
int(job.trigger.interval.total_seconds() * 0.75),
int(job.trigger.interval.total_seconds()))))
def __no_task(self):
for job in self.aps_scheduler.get_jobs():

View File

@ -1,10 +1,11 @@
# coding=utf-8
import signal
import warnings
import logging
import os
import io
import errno
from literals import EXIT_INTERRUPT, EXIT_NORMAL, EXIT_PORT_ALREADY_IN_USE_ERROR
from utilities.central import restart_bazarr, stop_bazarr
from waitress.server import create_server
from time import sleep
@ -17,10 +18,7 @@ from .database import close_database
from .app import create_app
app = create_app()
ui_bp.register_blueprint(api_bp, url_prefix='/api')
# Mute UserWarning with flask-restx and Flask >= 2.2.0. Will be raised as an exception in 2.3.0
# https://github.com/python-restx/flask-restx/issues/485
warnings.filterwarnings('ignore', message='The setup method ')
app.register_blueprint(api_bp, url_prefix=base_url.rstrip('/') + '/api')
app.register_blueprint(ui_bp, url_prefix=base_url.rstrip('/'))
@ -37,6 +35,7 @@ class Server:
self.connected = False
self.address = str(settings.general.ip)
self.port = int(args.port) if args.port else int(settings.general.port)
self.interrupted = False
while not self.connected:
sleep(0.1)
@ -54,17 +53,32 @@ class Server:
logging.exception("BAZARR cannot bind to specified IP, trying with default (0.0.0.0)")
self.address = '0.0.0.0'
self.connected = False
super(Server, self).__init__()
elif error.errno == errno.EADDRINUSE:
logging.exception("BAZARR cannot bind to specified TCP port, trying with default (6767)")
self.port = '6767'
self.connected = False
if self.port != '6767':
logging.exception("BAZARR cannot bind to specified TCP port, trying with default (6767)")
self.port = '6767'
self.connected = False
super(Server, self).__init__()
else:
logging.exception("BAZARR cannot bind to default TCP port (6767) because it's already in use, "
"exiting...")
self.shutdown(EXIT_PORT_ALREADY_IN_USE_ERROR)
else:
logging.exception("BAZARR cannot start because of unhandled exception.")
self.shutdown()
def interrupt_handler(self, signum, frame):
# print('Server signal interrupt handler called with signal', signum)
if not self.interrupted:
# ignore user hammering Ctrl-C; we heard you the first time!
self.interrupted = True
self.shutdown(EXIT_INTERRUPT)
def start(self):
logging.info(f'BAZARR is started and waiting for request on http://{self.server.effective_host}:'
f'{self.server.effective_port}')
signal.signal(signal.SIGINT, self.interrupt_handler)
try:
self.server.run()
except (KeyboardInterrupt, SystemExit):
@ -72,31 +86,19 @@ class Server:
except Exception:
pass
def shutdown(self):
try:
stop_file = io.open(os.path.join(args.config_dir, "bazarr.stop"), "w", encoding='UTF-8')
except Exception as e:
logging.error(f'BAZARR Cannot create stop file: {repr(e)}')
else:
logging.info('Bazarr is being shutdown...')
stop_file.write(str(''))
stop_file.close()
close_database()
self.server.close()
os._exit(0)
def close_all(self):
print("Closing database...")
close_database()
print("Closing webserver...")
self.server.close()
def shutdown(self, status=EXIT_NORMAL):
self.close_all()
stop_bazarr(status, False)
def restart(self):
try:
restart_file = io.open(os.path.join(args.config_dir, "bazarr.restart"), "w", encoding='UTF-8')
except Exception as e:
logging.error(f'BAZARR Cannot create restart file: {repr(e)}')
else:
logging.info('Bazarr is being restarted...')
restart_file.write(str(''))
restart_file.close()
close_database()
self.server.close()
os._exit(0)
self.close_all()
restart_bazarr()
webserver = Server()

View File

@ -12,7 +12,7 @@ from signalrcore.hub_connection_builder import HubConnectionBuilder
from collections import deque
from time import sleep
from constants import headers
from constants import HEADERS
from app.event_handler import event_stream
from sonarr.sync.episodes import sync_episodes, sync_one_episode
from sonarr.sync.series import update_series, update_one_series
@ -39,7 +39,7 @@ class SonarrSignalrClientLegacy:
self.session = Session()
self.session.timeout = 60
self.session.verify = False
self.session.headers = headers
self.session.headers = HEADERS
self.connection = None
self.connected = False
@ -162,7 +162,7 @@ class SonarrSignalrClient:
.with_url(f"{url_sonarr()}/signalr/messages?access_token={self.apikey_sonarr}",
options={
"verify_ssl": False,
"headers": headers
"headers": HEADERS
}) \
.with_automatic_reconnect({
"type": "raw",
@ -229,7 +229,7 @@ class RadarrSignalrClient:
.with_url(f"{url_radarr()}/signalr/messages?access_token={self.apikey_radarr}",
options={
"verify_ssl": False,
"headers": headers
"headers": HEADERS
}) \
.with_automatic_reconnect({
"type": "raw",

View File

@ -4,14 +4,17 @@ import os
import requests
import mimetypes
from flask import request, abort, render_template, Response, session, send_file, stream_with_context, Blueprint
from flask import (request, abort, render_template, Response, session, send_file, stream_with_context, Blueprint,
redirect)
from functools import wraps
from urllib.parse import unquote
from constants import headers
from constants import HEADERS
from literals import FILE_LOG
from sonarr.info import url_api_sonarr
from radarr.info import url_api_radarr
from utilities.helper import check_credentials
from utilities.central import get_log_file_path
from .config import settings, base_url
from .database import System
@ -63,6 +66,10 @@ def check_login(actual_method):
@ui_bp.route('/', defaults={'path': ''})
@ui_bp.route('/<path:path>')
def catch_all(path):
if path.startswith('login') and settings.auth.type not in ['basic', 'form']:
# login page has been accessed when no authentication is enabled
return redirect(base_url or "/", code=302)
auth = True
if settings.auth.type == 'basic':
auth = request.authorization
@ -98,9 +105,9 @@ def catch_all(path):
@check_login
@ui_bp.route('/bazarr.log')
@ui_bp.route('/' + FILE_LOG)
def download_log():
return send_file(os.path.join(args.config_dir, 'log', 'bazarr.log'), max_age=0, as_attachment=True)
return send_file(get_log_file_path(), max_age=0, as_attachment=True)
@check_login
@ -111,7 +118,7 @@ def series_images(url):
baseUrl = settings.sonarr.base_url
url_image = f'{url_api_sonarr()}{url.lstrip(baseUrl)}?apikey={apikey}'.replace('poster-250', 'poster-500')
try:
req = requests.get(url_image, stream=True, timeout=15, verify=False, headers=headers)
req = requests.get(url_image, stream=True, timeout=15, verify=False, headers=HEADERS)
except Exception:
return '', 404
else:
@ -125,7 +132,7 @@ def movies_images(url):
baseUrl = settings.radarr.base_url
url_image = f'{url_api_radarr()}{url.lstrip(baseUrl)}?apikey={apikey}'
try:
req = requests.get(url_image, stream=True, timeout=15, verify=False, headers=headers)
req = requests.get(url_image, stream=True, timeout=15, verify=False, headers=HEADERS)
except Exception:
return '', 404
else:
@ -162,25 +169,25 @@ def configured():
@ui_bp.route('/test/<protocol>/<path:url>', methods=['GET'])
def proxy(protocol, url):
if protocol.lower() not in ['http', 'https']:
return dict(status=False, error='Unsupported protocol')
return dict(status=False, error='Unsupported protocol', code=0)
url = f'{protocol}://{unquote(url)}'
params = request.args
try:
result = requests.get(url, params, allow_redirects=False, verify=False, timeout=5, headers=headers)
result = requests.get(url, params, allow_redirects=False, verify=False, timeout=5, headers=HEADERS)
except Exception as e:
return dict(status=False, error=repr(e))
else:
if result.status_code == 200:
try:
version = result.json()['version']
return dict(status=True, version=version)
return dict(status=True, version=version, code=result.status_code)
except Exception:
return dict(status=False, error='Error Occurred. Check your settings.')
return dict(status=False, error='Error Occurred. Check your settings.', code=result.status_code)
elif result.status_code == 401:
return dict(status=False, error='Access Denied. Check API key.')
return dict(status=False, error='Access Denied. Check API key.', code=result.status_code)
elif result.status_code == 404:
return dict(status=False, error='Cannot get version. Maybe unsupported legacy API call?')
return dict(status=False, error='Cannot get version. Maybe unsupported legacy API call?', code=result.status_code)
elif 300 <= result.status_code <= 399:
return dict(status=False, error='Wrong URL Base.')
return dict(status=False, error='Wrong URL Base.', code=result.status_code)
else:
return dict(status=False, error=result.raise_for_status())
return dict(status=False, error=result.raise_for_status(), code=result.status_code)

View File

@ -1,13 +1,12 @@
# coding=utf-8
import os
import re
# set Bazarr user-agent used to make requests
headers = {"User-Agent": os.environ["SZ_USER_AGENT"]}
# hearing-impaired detection regex
hi_regex = re.compile(r'[*¶♫♪].{3,}[*¶♫♪]|[\[\(\{].{3,}[\]\)\}](?<!{\\an\d})')
HEADERS = {"User-Agent": os.environ["SZ_USER_AGENT"]}
# minimum file size for Bazarr to consider it a video
MINIMUM_VIDEO_SIZE = 20480
# maximum size for a subtitles file
MAXIMUM_SUBTITLE_SIZE = 1 * 1024 * 1024

View File

@ -1,7 +1,6 @@
# coding=utf-8
import os
import io
import sys
import subprocess
import subliminal
@ -20,6 +19,10 @@ from utilities.backup import restore_from_backup
from app.database import init_db
from literals import (EXIT_CONFIG_CREATE_ERROR, ENV_BAZARR_ROOT_DIR, DIR_BACKUP, DIR_CACHE, DIR_CONFIG, DIR_DB, DIR_LOG,
DIR_RESTORE, EXIT_REQUIREMENTS_ERROR)
from utilities.central import make_bazarr_dir, restart_bazarr, stop_bazarr
# set start time global variable as epoch
global startTime
startTime = time.time()
@ -37,20 +40,15 @@ if not os.path.exists(args.config_dir):
os.mkdir(os.path.join(args.config_dir))
except OSError:
print("BAZARR The configuration directory doesn't exist and Bazarr cannot create it (permission issue?).")
exit(2)
stop_bazarr(EXIT_CONFIG_CREATE_ERROR)
if not os.path.exists(os.path.join(args.config_dir, 'config')):
os.mkdir(os.path.join(args.config_dir, 'config'))
if not os.path.exists(os.path.join(args.config_dir, 'db')):
os.mkdir(os.path.join(args.config_dir, 'db'))
if not os.path.exists(os.path.join(args.config_dir, 'log')):
os.mkdir(os.path.join(args.config_dir, 'log'))
if not os.path.exists(os.path.join(args.config_dir, 'cache')):
os.mkdir(os.path.join(args.config_dir, 'cache'))
if not os.path.exists(os.path.join(args.config_dir, 'backup')):
os.mkdir(os.path.join(args.config_dir, 'backup'))
if not os.path.exists(os.path.join(args.config_dir, 'restore')):
os.mkdir(os.path.join(args.config_dir, 'restore'))
os.environ[ENV_BAZARR_ROOT_DIR] = os.path.join(args.config_dir)
make_bazarr_dir(DIR_BACKUP)
make_bazarr_dir(DIR_CACHE)
make_bazarr_dir(DIR_CONFIG)
make_bazarr_dir(DIR_DB)
make_bazarr_dir(DIR_LOG)
make_bazarr_dir(DIR_RESTORE)
# set subliminal_patch hearing-impaired extension to use when naming subtitles
os.environ["SZ_HI_EXTENSION"] = settings.general.hi_extension
@ -99,19 +97,11 @@ if not args.no_update:
subprocess.check_output(pip_command, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
logging.exception(f'BAZARR requirements.txt installation result: {e.stdout}')
os._exit(1)
os._exit(EXIT_REQUIREMENTS_ERROR)
else:
logging.info('BAZARR requirements installed.')
try:
restart_file = io.open(os.path.join(args.config_dir, "bazarr.restart"), "w", encoding='UTF-8')
except Exception as e:
logging.error(f'BAZARR Cannot create restart file: {repr(e)}')
else:
logging.info('Bazarr is being restarted...')
restart_file.write(str(''))
restart_file.close()
os._exit(0)
restart_bazarr()
# change default base_url to ''
settings.general.base_url = settings.general.base_url.rstrip('/')

31
bazarr/literals.py Normal file
View File

@ -0,0 +1,31 @@
# coding=utf-8
# only primitive types can be specified here
# for other derived values, use constants.py
# bazarr environment variable names
ENV_STOPFILE = 'STOPFILE'
ENV_RESTARTFILE = 'RESTARTFILE'
ENV_BAZARR_ROOT_DIR = 'BAZARR_ROOT'
# bazarr subdirectories
DIR_BACKUP = 'backup'
DIR_CACHE = 'cache'
DIR_CONFIG = 'config'
DIR_DB = 'db'
DIR_LOG = 'log'
DIR_RESTORE = 'restore'
# bazarr special files
FILE_LOG = 'bazarr.log'
FILE_RESTART = 'bazarr.restart'
FILE_STOP = 'bazarr.stop'
# bazarr exit codes
EXIT_NORMAL = 0
EXIT_INTERRUPT = -100
EXIT_VALIDATION_ERROR = -101
EXIT_CONFIG_CREATE_ERROR = -102
EXIT_PYTHON_UPGRADE_NEEDED = -103
EXIT_REQUIREMENTS_ERROR = -104
EXIT_PORT_ALREADY_IN_USE_ERROR = -105

View File

@ -20,6 +20,7 @@ from app.get_args import args # noqa E402
from app.check_update import apply_update, check_releases, check_if_new_update # noqa E402
from app.config import settings, configure_proxy_func, base_url # noqa E402
from init import * # noqa E402
import logging # noqa E402
# Install downloaded update
if bazarr_version != '':
@ -40,18 +41,12 @@ from languages.get_languages import load_language_in_db # noqa E402
from app.signalr_client import sonarr_signalr_client, radarr_signalr_client # noqa E402
from app.server import webserver, app # noqa E402
from app.announcements import get_announcements_to_file # noqa E402
from utilities.central import stop_bazarr # noqa E402
from literals import EXIT_NORMAL # noqa E402
if args.create_db_revision:
try:
stop_file = io.open(os.path.join(args.config_dir, "bazarr.stop"), "w", encoding='UTF-8')
except Exception as e:
logging.error(f'BAZARR Cannot create stop file: {repr(e)}')
else:
create_db_revision(app)
logging.info('Bazarr is being shutdown...')
stop_file.write(str(''))
stop_file.close()
os._exit(0)
create_db_revision(app)
stop_bazarr(EXIT_NORMAL)
else:
migrate_db(app)

View File

@ -5,7 +5,7 @@ import logging
from app.config import settings
from radarr.info import url_api_radarr
from constants import headers
from constants import HEADERS
def browse_radarr_filesystem(path='#'):
@ -16,7 +16,7 @@ def browse_radarr_filesystem(path='#'):
f"includeFiles=false&apikey={settings.radarr.apikey}")
try:
r = requests.get(url_radarr_api_filesystem, timeout=int(settings.radarr.http_timeout), verify=False,
headers=headers)
headers=HEADERS)
r.raise_for_status()
except requests.exceptions.HTTPError:
logging.exception("BAZARR Error trying to get series from Radarr. Http error.")

View File

@ -3,12 +3,12 @@
import logging
import requests
import datetime
import json
from requests.exceptions import JSONDecodeError
from dogpile.cache import make_region
from app.config import settings, empty_values
from constants import headers
from constants import HEADERS
region = make_region().configure('dogpile.cache.memory')
@ -30,17 +30,17 @@ class GetRadarrInfo:
try:
rv = f"{url_radarr()}/api/system/status?apikey={settings.radarr.apikey}"
radarr_json = requests.get(rv, timeout=int(settings.radarr.http_timeout), verify=False,
headers=headers).json()
headers=HEADERS).json()
if 'version' in radarr_json:
radarr_version = radarr_json['version']
else:
raise json.decoder.JSONDecodeError
except json.decoder.JSONDecodeError:
raise JSONDecodeError
except JSONDecodeError:
try:
rv = f"{url_radarr()}/api/v3/system/status?apikey={settings.radarr.apikey}"
radarr_version = requests.get(rv, timeout=int(settings.radarr.http_timeout), verify=False,
headers=headers).json()['version']
except json.decoder.JSONDecodeError:
headers=HEADERS).json()['version']
except JSONDecodeError:
logging.debug('BAZARR cannot get Radarr version')
radarr_version = 'unknown'
except Exception:

View File

@ -5,7 +5,7 @@ import requests
from app.config import settings
from radarr.info import url_api_radarr
from constants import headers
from constants import HEADERS
def notify_radarr(radarr_id):
@ -15,6 +15,6 @@ def notify_radarr(radarr_id):
'name': 'RescanMovie',
'movieId': int(radarr_id)
}
requests.post(url, json=data, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers)
requests.post(url, json=data, timeout=int(settings.radarr.http_timeout), verify=False, headers=HEADERS)
except Exception:
logging.exception('BAZARR cannot notify Radarr')

View File

@ -8,7 +8,7 @@ from app.config import settings
from utilities.path_mappings import path_mappings
from app.database import TableMoviesRootfolder, TableMovies, database, delete, update, insert, select
from radarr.info import url_api_radarr
from constants import headers
from constants import HEADERS
def get_radarr_rootfolder():
@ -19,7 +19,7 @@ def get_radarr_rootfolder():
url_radarr_api_rootfolder = f"{url_api_radarr()}rootfolder?apikey={apikey_radarr}"
try:
rootfolder = requests.get(url_radarr_api_rootfolder, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers)
rootfolder = requests.get(url_radarr_api_rootfolder, timeout=int(settings.radarr.http_timeout), verify=False, headers=HEADERS)
except requests.exceptions.ConnectionError:
logging.exception("BAZARR Error trying to get rootfolder from Radarr. Connection Error.")
return []
@ -75,8 +75,8 @@ def check_radarr_rootfolder():
if not os.path.isdir(path_mappings.path_replace_movie(root_path)):
database.execute(
update(TableMoviesRootfolder)
.values(accessible=0, error='This Radarr root directory does not seems to be accessible by Please '
'check path mapping.')
.values(accessible=0, error='This Radarr root directory does not seem to be accessible by Bazarr. '
'Please check path mapping or if directory/drive is online.')
.where(TableMoviesRootfolder.id == item.id))
elif not os.access(path_mappings.path_replace_movie(root_path), os.W_OK):
database.execute(

View File

@ -21,10 +21,13 @@ from .parser import movieParser
bool_map = {"True": True, "False": False}
FEATURE_PREFIX = "SYNC_MOVIES "
def trace(message):
if settings.general.debug:
logging.debug(FEATURE_PREFIX + message)
def update_all_movies():
movies_full_scan_subtitles()
logging.info('BAZARR All existing movie subtitles indexed from disk.')
@ -63,6 +66,7 @@ def get_movie_monitored_status(movie_id):
else:
return bool_map[existing_movie_monitored[0]]
# Insert new movies in DB
def add_movie(added_movie, send_event):
try:
@ -158,7 +162,7 @@ def update_movies(send_event=True):
# Only movies that Radarr says have files downloaded will be kept up to date in the DB
if movie['hasFile'] is True:
if 'movieFile' in movie:
if sync_monitored:
if sync_monitored:
if get_movie_monitored_status(movie['tmdbId']) != movie['monitored']:
# monitored status is not the same as our DB
trace(f"{i}: (Monitor Status Mismatch) {movie['title']}")
@ -187,19 +191,21 @@ def update_movies(send_event=True):
add_movie(parsed_movie, send_event)
movies_added.append(parsed_movie['title'])
else:
trace(f"{i}: (Skipped File Missing) {movie['title']}")
files_missing += 1
trace(f"{i}: (Skipped File Missing) {movie['title']}")
files_missing += 1
if send_event:
hide_progress(id='movies_progress')
trace(f"Skipped {files_missing} file missing movies out of {i}")
trace(f"Skipped {files_missing} file missing movies out of {movies_count}")
if sync_monitored:
trace(f"Skipped {skipped_count} unmonitored movies out of {i}")
trace(f"Processed {i - files_missing - skipped_count} movies out of {i} " +
f"with {len(movies_added)} added, {len(movies_updated)} updated and {len(movies_deleted)} deleted")
trace(f"Skipped {skipped_count} unmonitored movies out of {movies_count}")
trace(f"Processed {movies_count - files_missing - skipped_count} movies out of {movies_count} "
f"with {len(movies_added)} added, {len(movies_updated)} updated and "
f"{len(movies_deleted)} deleted")
else:
trace(f"Processed {i - files_missing} movies out of {i} with {len(movies_added)} added and {len(movies_updated)} updated")
trace(f"Processed {movies_count - files_missing} movies out of {movies_count} with {len(movies_added)} added and "
f"{len(movies_updated)} updated")
logging.debug('BAZARR All movies synced from Radarr into database.')

View File

@ -115,27 +115,27 @@ def movieParser(movie, action, tags_dict, movie_default_profile, audio_profiles)
tags = [d['label'] for d in tags_dict if d['id'] in movie['tags']]
parsed_movie = {'radarrId': int(movie["id"]),
'title': movie["title"],
'path': os.path.join(movie["path"], movie['movieFile']['relativePath']),
'tmdbId': str(movie["tmdbId"]),
'poster': poster,
'fanart': fanart,
'audio_language': str(audio_language),
'sceneName': sceneName,
'monitored': str(bool(movie['monitored'])),
'year': str(movie['year']),
'sortTitle': movie['sortTitle'],
'alternativeTitles': alternativeTitles,
'format': format,
'resolution': resolution,
'video_codec': videoCodec,
'audio_codec': audioCodec,
'overview': overview,
'imdbId': imdbId,
'movie_file_id': int(movie['movieFile']['id']),
'tags': str(tags),
'file_size': movie['movieFile']['size']}
'title': movie["title"],
'path': os.path.join(movie["path"], movie['movieFile']['relativePath']),
'tmdbId': str(movie["tmdbId"]),
'poster': poster,
'fanart': fanart,
'audio_language': str(audio_language),
'sceneName': sceneName,
'monitored': str(bool(movie['monitored'])),
'year': str(movie['year']),
'sortTitle': movie['sortTitle'],
'alternativeTitles': alternativeTitles,
'format': format,
'resolution': resolution,
'video_codec': videoCodec,
'audio_codec': audioCodec,
'overview': overview,
'imdbId': imdbId,
'movie_file_id': int(movie['movieFile']['id']),
'tags': str(tags),
'file_size': movie['movieFile']['size']}
if action == 'insert':
parsed_movie['subtitles'] = '[]'
parsed_movie['profileId'] = movie_default_profile

View File

@ -5,7 +5,7 @@ import logging
from app.config import settings
from radarr.info import get_radarr_info, url_api_radarr
from constants import headers
from constants import HEADERS
def get_profile_list():
@ -16,7 +16,7 @@ def get_profile_list():
f"apikey={apikey_radarr}")
try:
profiles_json = requests.get(url_radarr_api_movies, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers)
profiles_json = requests.get(url_radarr_api_movies, timeout=int(settings.radarr.http_timeout), verify=False, headers=HEADERS)
except requests.exceptions.ConnectionError:
logging.exception("BAZARR Error trying to get profiles from Radarr. Connection Error.")
except requests.exceptions.Timeout:
@ -45,7 +45,7 @@ def get_tags():
url_radarr_api_series = f"{url_api_radarr()}tag?apikey={apikey_radarr}"
try:
tagsDict = requests.get(url_radarr_api_series, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers)
tagsDict = requests.get(url_radarr_api_series, timeout=int(settings.radarr.http_timeout), verify=False, headers=HEADERS)
except requests.exceptions.ConnectionError:
logging.exception("BAZARR Error trying to get tags from Radarr. Connection Error.")
return []
@ -69,7 +69,7 @@ def get_movies_from_radarr_api(apikey_radarr, radarr_id=None):
url_radarr_api_movies = f'{url_api_radarr()}movie{f"/{radarr_id}" if radarr_id else ""}?apikey={apikey_radarr}'
try:
r = requests.get(url_radarr_api_movies, timeout=int(settings.radarr.http_timeout), verify=False, headers=headers)
r = requests.get(url_radarr_api_movies, timeout=int(settings.radarr.http_timeout), verify=False, headers=HEADERS)
if r.status_code == 404:
return
r.raise_for_status()
@ -100,7 +100,7 @@ def get_history_from_radarr_api(apikey_radarr, movie_id):
try:
r = requests.get(url_radarr_api_history, timeout=int(settings.sonarr.http_timeout), verify=False,
headers=headers)
headers=HEADERS)
r.raise_for_status()
except requests.exceptions.HTTPError:
logging.exception("BAZARR Error trying to get history from Radarr. Http error.")

View File

@ -5,7 +5,7 @@ import logging
from app.config import settings
from sonarr.info import url_api_sonarr
from constants import headers
from constants import HEADERS
def browse_sonarr_filesystem(path='#'):
@ -15,7 +15,7 @@ def browse_sonarr_filesystem(path='#'):
f"includeFiles=false&apikey={settings.sonarr.apikey}")
try:
r = requests.get(url_sonarr_api_filesystem, timeout=int(settings.sonarr.http_timeout), verify=False,
headers=headers)
headers=HEADERS)
r.raise_for_status()
except requests.exceptions.HTTPError:
logging.exception("BAZARR Error trying to get series from Sonarr. Http error.")

View File

@ -3,12 +3,12 @@
import logging
import requests
import datetime
import json
from requests.exceptions import JSONDecodeError
from dogpile.cache import make_region
from app.config import settings, empty_values
from constants import headers
from constants import HEADERS
region = make_region().configure('dogpile.cache.memory')
@ -30,17 +30,17 @@ class GetSonarrInfo:
try:
sv = f"{url_sonarr()}/api/system/status?apikey={settings.sonarr.apikey}"
sonarr_json = requests.get(sv, timeout=int(settings.sonarr.http_timeout), verify=False,
headers=headers).json()
headers=HEADERS).json()
if 'version' in sonarr_json:
sonarr_version = sonarr_json['version']
else:
raise json.decoder.JSONDecodeError
except json.decoder.JSONDecodeError:
raise JSONDecodeError
except JSONDecodeError:
try:
sv = f"{url_sonarr()}/api/v3/system/status?apikey={settings.sonarr.apikey}"
sonarr_version = requests.get(sv, timeout=int(settings.sonarr.http_timeout), verify=False,
headers=headers).json()['version']
except json.decoder.JSONDecodeError:
headers=HEADERS).json()['version']
except JSONDecodeError:
logging.debug('BAZARR cannot get Sonarr version')
sonarr_version = 'unknown'
except Exception:

View File

@ -5,7 +5,7 @@ import requests
from app.config import settings
from sonarr.info import url_api_sonarr
from constants import headers
from constants import HEADERS
def notify_sonarr(sonarr_series_id):
@ -15,6 +15,6 @@ def notify_sonarr(sonarr_series_id):
'name': 'RescanSeries',
'seriesId': int(sonarr_series_id)
}
requests.post(url, json=data, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers)
requests.post(url, json=data, timeout=int(settings.sonarr.http_timeout), verify=False, headers=HEADERS)
except Exception:
logging.exception('BAZARR cannot notify Sonarr')

View File

@ -8,7 +8,7 @@ from app.config import settings
from app.database import TableShowsRootfolder, TableShows, database, insert, update, delete, select
from utilities.path_mappings import path_mappings
from sonarr.info import url_api_sonarr
from constants import headers
from constants import HEADERS
def get_sonarr_rootfolder():
@ -19,7 +19,7 @@ def get_sonarr_rootfolder():
url_sonarr_api_rootfolder = f"{url_api_sonarr()}rootfolder?apikey={apikey_sonarr}"
try:
rootfolder = requests.get(url_sonarr_api_rootfolder, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers)
rootfolder = requests.get(url_sonarr_api_rootfolder, timeout=int(settings.sonarr.http_timeout), verify=False, headers=HEADERS)
except requests.exceptions.ConnectionError:
logging.exception("BAZARR Error trying to get rootfolder from Sonarr. Connection Error.")
return []
@ -75,8 +75,8 @@ def check_sonarr_rootfolder():
if not os.path.isdir(path_mappings.path_replace(root_path)):
database.execute(
update(TableShowsRootfolder)
.values(accessible=0, error='This Sonarr root directory does not seems to be accessible by Bazarr. '
'Please check path mapping.')
.values(accessible=0, error='This Sonarr root directory does not seem to be accessible by Bazarr. '
'Please check path mapping or if directory/drive is online.')
.where(TableShowsRootfolder.id == item.id))
elif not os.access(path_mappings.path_replace(root_path), os.W_OK):
database.execute(

View File

@ -12,7 +12,7 @@ from utilities.path_mappings import path_mappings
from subtitles.indexer.series import store_subtitles, series_full_scan_subtitles
from subtitles.mass_download import episode_download_subtitles
from app.event_handler import event_stream
from sonarr.info import get_sonarr_info, url_sonarr
from sonarr.info import get_sonarr_info
from .parser import episodeParser
from .utils import get_episodes_from_sonarr_api, get_episodesFiles_from_sonarr_api
@ -21,10 +21,13 @@ from .utils import get_episodes_from_sonarr_api, get_episodesFiles_from_sonarr_a
bool_map = {"True": True, "False": False}
FEATURE_PREFIX = "SYNC_EPISODES "
def trace(message):
if settings.general.debug:
logging.debug(FEATURE_PREFIX + message)
def get_episodes_monitored_table(series_id):
episodes_monitored = database.execute(
select(TableEpisodes.episode_file_id, TableEpisodes.monitored)
@ -32,7 +35,8 @@ def get_episodes_monitored_table(series_id):
.all()
episode_dict = dict((x, y) for x, y in episodes_monitored)
return episode_dict
def update_all_episodes():
series_full_scan_subtitles()
logging.info('BAZARR All existing episode subtitles indexed from disk.')
@ -74,7 +78,6 @@ def sync_episodes(series_id, send_event=True):
if item:
episode['episodeFile'] = item[0]
sync_monitored = settings.sonarr.sync_only_monitored_series and settings.sonarr.sync_only_monitored_episodes
if sync_monitored:
episodes_monitored = get_episodes_monitored_table(series_id)
@ -122,7 +125,7 @@ def sync_episodes(series_id, send_event=True):
episodes_to_add.append(episodeParser(episode))
else:
return
if sync_monitored:
# try to avoid unnecessary database calls
if settings.general.debug:
@ -175,7 +178,6 @@ def sync_episodes(series_id, send_event=True):
def sync_one_episode(episode_id, defer_search=False):
logging.debug(f'BAZARR syncing this specific episode from Sonarr: {episode_id}')
url = url_sonarr()
apikey_sonarr = settings.sonarr.apikey
# Check if there's a row in database for this episode ID

View File

@ -5,7 +5,6 @@ import logging
from sqlalchemy.exc import IntegrityError
from app.config import settings
from sonarr.info import url_sonarr
from subtitles.indexer.series import list_missing_subtitles
from sonarr.rootfolder import check_sonarr_rootfolder
from app.database import TableShows, TableLanguagesProfiles, database, insert, update, delete, select
@ -20,10 +19,13 @@ from .utils import get_profile_list, get_tags, get_series_from_sonarr_api
bool_map = {"True": True, "False": False}
FEATURE_PREFIX = "SYNC_SERIES "
def trace(message):
if settings.general.debug:
logging.debug(FEATURE_PREFIX + message)
def get_series_monitored_table():
series_monitored = database.execute(
select(TableShows.tvdbId, TableShows.monitored))\
@ -31,6 +33,7 @@ def get_series_monitored_table():
series_dict = dict((x, y) for x, y in series_monitored)
return series_dict
def update_series(send_event=True):
check_sonarr_rootfolder()
apikey_sonarr = settings.sonarr.apikey
@ -74,7 +77,7 @@ def update_series(send_event=True):
series_monitored = get_series_monitored_table()
skipped_count = 0
trace(f"Starting sync for {series_count} shows")
for i, show in enumerate(series):
if send_event:
show_progress(id='series_progress',
@ -152,7 +155,7 @@ def update_series(send_event=True):
removed_series = list(set(current_shows_db) - set(current_shows_sonarr))
for series in removed_series:
# try to avoid unnecessary database calls
# try to avoid unnecessary database calls
if settings.general.debug:
series_title = database.execute(select(TableShows.title).where(TableShows.sonarrSeriesId == series)).first()[0]
trace(f"Deleting {series_title}")

View File

@ -5,7 +5,7 @@ import logging
from app.config import settings
from sonarr.info import get_sonarr_info, url_api_sonarr
from constants import headers
from constants import HEADERS
def get_profile_list():
@ -23,7 +23,7 @@ def get_profile_list():
try:
profiles_json = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=False,
headers=headers)
headers=HEADERS)
except requests.exceptions.ConnectionError:
logging.exception("BAZARR Error trying to get profiles from Sonarr. Connection Error.")
return None
@ -53,7 +53,7 @@ def get_tags():
url_sonarr_api_series = f"{url_api_sonarr()}tag?apikey={apikey_sonarr}"
try:
tagsDict = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers)
tagsDict = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=False, headers=HEADERS)
except requests.exceptions.ConnectionError:
logging.exception("BAZARR Error trying to get tags from Sonarr. Connection Error.")
return []
@ -71,7 +71,7 @@ def get_series_from_sonarr_api(apikey_sonarr, sonarr_series_id=None):
url_sonarr_api_series = (f"{url_api_sonarr()}series/{sonarr_series_id if sonarr_series_id else ''}?"
f"apikey={apikey_sonarr}")
try:
r = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers)
r = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=False, headers=HEADERS)
r.raise_for_status()
except requests.exceptions.HTTPError as e:
if e.response.status_code:
@ -110,7 +110,7 @@ def get_episodes_from_sonarr_api(apikey_sonarr, series_id=None, episode_id=None)
return
try:
r = requests.get(url_sonarr_api_episode, timeout=int(settings.sonarr.http_timeout), verify=False, headers=headers)
r = requests.get(url_sonarr_api_episode, timeout=int(settings.sonarr.http_timeout), verify=False, headers=HEADERS)
r.raise_for_status()
except requests.exceptions.HTTPError:
logging.exception("BAZARR Error trying to get episodes from Sonarr. Http error.")
@ -144,7 +144,7 @@ def get_episodesFiles_from_sonarr_api(apikey_sonarr, series_id=None, episode_fil
try:
r = requests.get(url_sonarr_api_episodeFiles, timeout=int(settings.sonarr.http_timeout), verify=False,
headers=headers)
headers=HEADERS)
r.raise_for_status()
except requests.exceptions.HTTPError:
logging.exception("BAZARR Error trying to get episodeFiles from Sonarr. Http error.")
@ -173,7 +173,7 @@ def get_history_from_sonarr_api(apikey_sonarr, episode_id):
try:
r = requests.get(url_sonarr_api_history, timeout=int(settings.sonarr.http_timeout), verify=False,
headers=headers)
headers=HEADERS)
r.raise_for_status()
except requests.exceptions.HTTPError:
logging.exception("BAZARR Error trying to get history from Sonarr. Http error.")

View File

@ -24,8 +24,9 @@ from .processing import process_subtitle
@update_pools
def generate_subtitles(path, languages, audio_language, sceneName, title, media_type,
forced_minimum_score=None, is_upgrade=False, profile_id=None, check_if_still_required=False):
def generate_subtitles(path, languages, audio_language, sceneName, title, media_type, forced_minimum_score=None,
is_upgrade=False, profile_id=None, check_if_still_required=False,
previous_subtitles_to_delete=None):
if not languages:
return None
@ -87,6 +88,13 @@ def generate_subtitles(path, languages, audio_language, sceneName, title, media_
fld = get_target_folder(path)
chmod = int(settings.general.chmod, 8) if not sys.platform.startswith(
'win') and settings.general.chmod_enabled else None
if is_upgrade and previous_subtitles_to_delete:
try:
# delete previously downloaded subtitles in case of an upgrade to prevent edge loop
# issue.
os.remove(previous_subtitles_to_delete)
except (OSError, FileNotFoundError):
pass
saved_subtitles = save_subtitles(video.original_path, subtitles,
single=settings.general.single_language,
tags=None, # fixme

View File

@ -264,7 +264,10 @@ def list_missing_subtitles_movies(no=None, send_event=True):
event_stream(type='badges')
def movies_full_scan_subtitles(use_cache=settings.radarr.use_ffprobe_cache):
def movies_full_scan_subtitles(use_cache=None):
if use_cache is None:
use_cache = settings.radarr.use_ffprobe_cache
movies = database.execute(
select(TableMovies.path))\
.all()

View File

@ -266,7 +266,10 @@ def list_missing_subtitles(no=None, epno=None, send_event=True):
event_stream(type='badges')
def series_full_scan_subtitles(use_cache=settings.sonarr.use_ffprobe_cache):
def series_full_scan_subtitles(use_cache=None):
if use_cache is None:
use_cache = settings.sonarr.use_ffprobe_cache
episodes = database.execute(
select(TableEpisodes.path))\
.all()

View File

@ -9,8 +9,8 @@ from subliminal_patch import core
from subzero.language import Language
from charset_normalizer import detect
from constants import MAXIMUM_SUBTITLE_SIZE
from app.config import settings
from constants import hi_regex
from utilities.path_mappings import path_mappings
@ -68,7 +68,7 @@ def guess_external_subtitles(dest_folder, subtitles, media_type, previously_inde
forced = True if os.path.splitext(os.path.splitext(subtitle)[0])[1] == '.forced' else False
# to improve performance, skip detection of files larger that 1M
if os.path.getsize(subtitle_path) > 1 * 1024 * 1024:
if os.path.getsize(subtitle_path) > MAXIMUM_SUBTITLE_SIZE:
logging.debug(f"BAZARR subtitles file is too large to be text based. Skipping this file: "
f"{subtitle_path}")
continue
@ -119,7 +119,7 @@ def guess_external_subtitles(dest_folder, subtitles, media_type, previously_inde
# check if file exist:
if os.path.exists(subtitle_path) and os.path.splitext(subtitle_path)[1] in core.SUBTITLE_EXTENSIONS:
# to improve performance, skip detection of files larger that 1M
if os.path.getsize(subtitle_path) > 1 * 1024 * 1024:
if os.path.getsize(subtitle_path) > MAXIMUM_SUBTITLE_SIZE:
logging.debug(f"BAZARR subtitles file is too large to be text based. Skipping this file: "
f"{subtitle_path}")
continue
@ -136,6 +136,6 @@ def guess_external_subtitles(dest_folder, subtitles, media_type, previously_inde
continue
text = text.decode(encoding)
if bool(re.search(hi_regex, text)):
if bool(re.search(core.HI_REGEX, text)):
subtitles[subtitle] = Language.rebuild(subtitles[subtitle], forced=False, hi=True)
return subtitles

View File

@ -18,7 +18,7 @@ from app.config import get_scores, settings, get_array_from
from utilities.helper import get_target_folder, force_unicode
from app.database import get_profiles_list
from .pool import update_pools, _get_pool, _init_pool
from .pool import update_pools, _get_pool
from .utils import get_video, _get_lang_obj, _get_scores, _set_forced_providers
from .processing import process_subtitle
@ -46,21 +46,7 @@ def manual_search(path, profile_id, providers, sceneName, title, media_type):
try:
if providers:
subtitles = list_all_subtitles([video], language_set, pool)
if 'subscene' in providers:
s_pool = _init_pool("movie", profile_id, {"subscene"})
subscene_language_set = set()
for language in language_set:
if language.forced:
subscene_language_set.add(language)
if len(subscene_language_set):
s_pool.provider_configs.update({"subscene": {"only_foreign": True}})
subtitles_subscene = list_all_subtitles([video], subscene_language_set, s_pool)
s_pool.provider_configs.update({"subscene": {"only_foreign": False}})
subtitles[video] += subtitles_subscene[video]
else:
subtitles = []
logging.info("BAZARR All providers are throttled")
return 'All providers are throttled'
except Exception:

View File

@ -3,9 +3,11 @@
from .ffprobe import refine_from_ffprobe
from .database import refine_from_db
from .arr_history import refine_from_arr_history
from .anidb import refine_from_anidb
registered = {
"database": refine_from_db,
"ffprobe": refine_from_ffprobe,
"arr_history": refine_from_arr_history,
"anidb": refine_from_anidb,
}

View File

@ -0,0 +1,140 @@
# coding=utf-8
# fmt: off
import logging
import requests
from collections import namedtuple
from datetime import timedelta
from requests.exceptions import HTTPError
from app.config import settings
from subliminal import Episode, region
try:
from lxml import etree
except ImportError:
try:
import xml.etree.cElementTree as etree
except ImportError:
import xml.etree.ElementTree as etree
refined_providers = {'animetosho'}
api_url = 'http://api.anidb.net:9001/httpapi'
class AniDBClient(object):
def __init__(self, api_client_key=None, api_client_ver=1, session=None):
self.session = session or requests.Session()
self.api_client_key = api_client_key
self.api_client_ver = api_client_ver
AnimeInfo = namedtuple('AnimeInfo', ['anime', 'episode_offset'])
@region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds())
def get_series_mappings(self):
r = self.session.get(
'https://raw.githubusercontent.com/Anime-Lists/anime-lists/master/anime-list.xml',
timeout=10
)
r.raise_for_status()
return r.content
@region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds())
def get_series_id(self, mappings, tvdb_series_season, tvdb_series_id, episode):
# Enrich the collection of anime with the episode offset
animes = [
self.AnimeInfo(anime, int(anime.attrib.get('episodeoffset', 0)))
for anime in mappings.findall(
f".//anime[@tvdbid='{tvdb_series_id}'][@defaulttvdbseason='{tvdb_series_season}']"
)
]
if not animes:
return None, None
# Sort the anime by offset in ascending order
animes.sort(key=lambda a: a.episode_offset)
# Different from Tvdb, Anidb have different ids for the Parts of a season
anidb_id = None
offset = 0
for index, anime_info in enumerate(animes):
anime, episode_offset = anime_info
anidb_id = int(anime.attrib.get('anidbid'))
if episode > episode_offset:
anidb_id = anidb_id
offset = episode_offset
return anidb_id, episode - offset
@region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds())
def get_series_episodes_ids(self, tvdb_series_id, season, episode):
mappings = etree.fromstring(self.get_series_mappings())
series_id, episode_no = self.get_series_id(mappings, season, tvdb_series_id, episode)
if not series_id:
return None, None
episodes = etree.fromstring(self.get_episodes(series_id))
return series_id, int(episodes.find(f".//episode[epno='{episode_no}']").attrib.get('id'))
@region.cache_on_arguments(expiration_time=timedelta(days=1).total_seconds())
def get_episodes(self, series_id):
r = self.session.get(
api_url,
params={
'request': 'anime',
'client': self.api_client_key,
'clientver': self.api_client_ver,
'protover': 1,
'aid': series_id
},
timeout=10)
r.raise_for_status()
xml_root = etree.fromstring(r.content)
response_code = xml_root.attrib.get('code')
if response_code == '500':
raise HTTPError('AniDB API Abuse detected. Banned status.')
elif response_code == '302':
raise HTTPError('AniDB API Client error. Client is disabled or does not exists.')
episode_elements = xml_root.find('episodes')
if not episode_elements:
raise ValueError
return etree.tostring(episode_elements, encoding='utf8', method='xml')
def refine_from_anidb(path, video):
if not isinstance(video, Episode) or not video.series_tvdb_id:
logging.debug(f'Video is not an Anime TV series, skipping refinement for {video}')
return
if refined_providers.intersection(settings.general.enabled_providers) and video.series_anidb_id is None:
refine_anidb_ids(video)
def refine_anidb_ids(video):
anidb_client = AniDBClient(settings.anidb.api_client, settings.anidb.api_client_ver)
season = video.season if video.season else 0
anidb_series_id, anidb_episode_id = anidb_client.get_series_episodes_ids(video.series_tvdb_id, season, video.episode)
if not anidb_episode_id:
logging.error(f'Could not find anime series {video.series}')
return video
video.series_anidb_id = anidb_series_id
video.series_anidb_episode_id = anidb_episode_id

View File

@ -26,8 +26,19 @@ def sync_subtitles(video_path, srt_path, srt_lang, forced, percent_score, sonarr
if not use_subsync_threshold or (use_subsync_threshold and percent_score < float(subsync_threshold)):
subsync = SubSyncer()
subsync.sync(video_path=video_path, srt_path=srt_path, srt_lang=srt_lang,
sonarr_series_id=sonarr_series_id, sonarr_episode_id=sonarr_episode_id, radarr_id=radarr_id)
sync_kwargs = {
'video_path': video_path,
'srt_path': srt_path,
'srt_lang': srt_lang,
'max_offset_seconds': str(settings.subsync.max_offset_seconds),
'no_fix_framerate': settings.subsync.no_fix_framerate,
'gss': settings.subsync.gss,
'reference': None, # means choose automatically within video file
'sonarr_series_id': sonarr_series_id,
'sonarr_episode_id': sonarr_episode_id,
'radarr_id': radarr_id,
}
subsync.sync(**sync_kwargs)
del subsync
gc.collect()
return True

View File

@ -30,12 +30,18 @@ class SubSyncer:
self.vad = 'subs_then_webrtc'
self.log_dir_path = os.path.join(args.config_dir, 'log')
def sync(self, video_path, srt_path, srt_lang, sonarr_series_id=None, sonarr_episode_id=None, radarr_id=None,
reference=None, max_offset_seconds=str(settings.subsync.max_offset_seconds),
no_fix_framerate=settings.subsync.no_fix_framerate, gss=settings.subsync.gss):
def sync(self, video_path, srt_path, srt_lang,
max_offset_seconds, no_fix_framerate, gss, reference=None,
sonarr_series_id=None, sonarr_episode_id=None, radarr_id=None):
self.reference = video_path
self.srtin = srt_path
self.srtout = f'{os.path.splitext(self.srtin)[0]}.synced.srt'
if self.srtin.casefold().endswith('.ass'):
# try to preserve original subtitle style
# ffmpeg will be able to handle this automatically as long as it has the libass filter
extension = '.ass'
else:
extension = '.srt'
self.srtout = f'{os.path.splitext(self.srtin)[0]}.synced{extension}'
self.args = None
ffprobe_exe = get_binary('ffprobe')

View File

@ -110,7 +110,9 @@ def upgrade_subtitles():
episode['seriesTitle'],
'series',
forced_minimum_score=int(episode['score']),
is_upgrade=True))
is_upgrade=True,
previous_subtitles_to_delete=path_mappings.path_replace(
episode['subtitles_path'])))
if result:
if isinstance(result, list) and len(result):
@ -195,7 +197,9 @@ def upgrade_subtitles():
movie['title'],
'movie',
forced_minimum_score=int(movie['score']),
is_upgrade=True))
is_upgrade=True,
previous_subtitles_to_delete=path_mappings.path_replace_movie(
movie['subtitles_path'])))
if result:
if isinstance(result, list) and len(result):
result = result[0]

View File

@ -97,7 +97,6 @@ def _set_forced_providers(pool, also_forced=False, forced_required=False):
pool.provider_configs.update(
{
"podnapisi": {'also_foreign': also_forced, "only_foreign": forced_required},
"subscene": {"only_foreign": forced_required},
"opensubtitles": {'also_foreign': also_forced, "only_foreign": forced_required}
}
)

View File

@ -1,7 +1,6 @@
# coding=utf-8
import os
import io
import sqlite3
import shutil
import logging
@ -12,6 +11,7 @@ from glob import glob
from app.get_args import args
from app.config import settings
from utilities.central import restart_bazarr
def get_backup_path():
@ -33,7 +33,7 @@ def get_restore_path():
def get_backup_files(fullpath=True):
backup_file_pattern = os.path.join(get_backup_path(), 'bazarr_backup_v*.zip')
file_list = glob(backup_file_pattern)
file_list.sort(key=os.path.getmtime)
file_list.sort(key=os.path.getmtime, reverse=True)
if fullpath:
return file_list
else:
@ -133,16 +133,7 @@ def restore_from_backup():
logging.exception(f'Unable to delete {dest_database_path}')
logging.info('Backup restored successfully. Bazarr will restart.')
try:
restart_file = io.open(os.path.join(args.config_dir, "bazarr.restart"), "w", encoding='UTF-8')
except Exception as e:
logging.error(f'BAZARR Cannot create restart file: {repr(e)}')
else:
logging.info('Bazarr is being restarted...')
restart_file.write('')
restart_file.close()
os._exit(0)
restart_bazarr()
elif os.path.isfile(restore_config_path) or os.path.isfile(restore_database_path):
logging.debug('Cannot restore a partial backup. You must have both config and database.')
else:

View File

@ -0,0 +1,61 @@
# coding=utf-8
# only methods can be specified here that do not cause other moudules to be loaded
# for other methods that use settings, etc., use utilities/helper.py
import contextlib
import logging
import os
from pathlib import Path
from literals import ENV_BAZARR_ROOT_DIR, DIR_LOG, ENV_STOPFILE, ENV_RESTARTFILE, EXIT_NORMAL, FILE_LOG
def get_bazarr_dir(sub_dir):
path = os.path.join(os.environ[ENV_BAZARR_ROOT_DIR], sub_dir)
return path
def make_bazarr_dir(sub_dir):
path = get_bazarr_dir(sub_dir)
if not os.path.exists(path):
os.mkdir(path)
def get_log_file_path():
path = os.path.join(get_bazarr_dir(DIR_LOG), FILE_LOG)
return path
def get_stop_file_path():
return os.environ[ENV_STOPFILE]
def get_restart_file_path():
return os.environ[ENV_RESTARTFILE]
def stop_bazarr(status_code=EXIT_NORMAL, exit_main=True):
try:
with open(get_stop_file_path(), 'w', encoding='UTF-8') as file:
# write out status code for final exit
file.write(f'{status_code}\n')
file.close()
except Exception as e:
logging.error(f'BAZARR Cannot create stop file: {repr(e)}')
logging.info('Bazarr is being shutdown...')
if exit_main:
raise SystemExit(status_code)
def restart_bazarr():
try:
Path(get_restart_file_path()).touch()
except Exception as e:
logging.error(f'BAZARR Cannot create restart file: {repr(e)}')
logging.info('Bazarr is being restarted...')
# Wrap the SystemExit for a graceful restart. The SystemExit still performs the cleanup but the traceback is omitted
# preventing to throw the exception to the caller but still terminates the Python process with the desired Exit Code
with contextlib.suppress(SystemExit):
raise SystemExit(EXIT_NORMAL)

View File

@ -16,7 +16,7 @@ def _escape(in_str):
def pp_replace(pp_command, episode, subtitles, language, language_code2, language_code3, episode_language,
episode_language_code2, episode_language_code3, score, subtitle_id, provider, uploader,
episode_language_code2, episode_language_code3, score, subtitle_id, provider, uploader,
release_info, series_id, episode_id):
pp_command = re.sub(r'[\'"]?{{directory}}[\'"]?', _escape(os.path.dirname(episode)), pp_command)
pp_command = re.sub(r'[\'"]?{{episode}}[\'"]?', _escape(episode), pp_command)

View File

@ -270,7 +270,7 @@ def parse_video_metadata(file, file_size, episode_file_id=None, movie_file_id=No
if not os.path.exists(file):
logging.error(f'Video file "{file}" cannot be found for analysis')
return None
# if we have ffprobe available
if ffprobe_path:
try:

View File

@ -9,4 +9,4 @@ From newest to oldest:
{{#each commits}}
- {{subject}}{{#if href}} [{{shorthash}}]({{href}}){{/if}}
{{/each}}
{{/each}}
{{/each}}

View File

@ -9,4 +9,4 @@ From newest to oldest:
{{#each commits}}
- {{subject}}{{#if href}} [{{shorthash}}]({{href}}){{/if}}
{{/each}}
{{/each}}
{{/each}}

View File

@ -0,0 +1,18 @@
# Bazarr dependencies
subliminal_patch
subzero
py-pretty==1 # modified version to support Python 3
# Bazarr modified dependencies
signalr-client-threads==0.0.12 # Modified to work with Sonarr v3. Not used anymore with v4
Flask-Compress==1.14 # modified to import brotli only if required
# Required-by: signalr-client-threads
sseclient==0.0.27 # Modified to work with Sonarr v3
# Required-by: subliminal_patch
deathbycaptcha # unknown version, only found on gist
git+https://github.com/pannal/libfilebot#egg=libfilebot
git+https://github.com/RobinDavid/pyADS.git@28a2f6dbfb357f85b2c2f49add770b336e88840d#egg=pyads
py7zr==0.7.0 # modified to prevent importing of modules that can't be vendored
subliminal==2.1.0 # modified specifically for Bazarr

View File

@ -0,0 +1 @@
__version__ = "1.14"

View File

@ -63,9 +63,14 @@ class Compress(object):
def init_app(self, app):
defaults = [
('COMPRESS_MIMETYPES', ['text/html', 'text/css', 'text/xml',
'application/json',
'application/javascript']),
('COMPRESS_MIMETYPES', [
'application/javascript', # Obsolete (RFC 9239)
'application/json',
'text/css',
'text/html',
'text/javascript',
'text/xml',
]),
('COMPRESS_LEVEL', 6),
('COMPRESS_BR_LEVEL', 4),
('COMPRESS_BR_MODE', 0),

View File

@ -50,7 +50,7 @@ def default_xattr(fn):
XATTR_MAP = {
"default": (
default_xattr,
lambda result: re.search('(?um)(net\.filebot\.filename(?=="|: )[=:" ]+|Attribute.+:\s)([^"\n\r\0]+)',
lambda result: re.search(r'(?um)(net\.filebot\.filename(?=="|: )[=:" ]+|Attribute.+:\s)([^"\n\r\0]+)',
result).group(2)
),
# "darwin": (
@ -60,7 +60,7 @@ XATTR_MAP = {
# ),
"darwin": (
lambda fn: ["filebot", "-script", "fn:xattr", fn],
lambda result: re.search('(?um)(net\.filebot\.filename(?=="|: )[=:" ]+|Attribute.+:\s)([^"\n\r\0]+)',
lambda result: re.search(r'(?um)(net\.filebot\.filename(?=="|: )[=:" ]+|Attribute.+:\s)([^"\n\r\0]+)',
result).group(2)
),
"win32": (

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from babelfish import LanguageReverseConverter
from ..exceptions import ConfigurationError
class LegendasTVConverter(LanguageReverseConverter):
def __init__(self):
self.from_legendastv = {1: ('por', 'BR'), 2: ('eng',), 3: ('spa',), 4: ('fra',), 5: ('deu',), 6: ('jpn',),
7: ('dan',), 8: ('nor',), 9: ('swe',), 10: ('por',), 11: ('ara',), 12: ('ces',),
13: ('zho',), 14: ('kor',), 15: ('bul',), 16: ('ita',), 17: ('pol',)}
self.to_legendastv = {v: k for k, v in self.from_legendastv.items()}
self.codes = set(self.from_legendastv.keys())
def convert(self, alpha3, country=None, script=None):
if (alpha3, country) in self.to_legendastv:
return self.to_legendastv[(alpha3, country)]
if (alpha3,) in self.to_legendastv:
return self.to_legendastv[(alpha3,)]
raise ConfigurationError('Unsupported language code for legendastv: %s, %s, %s' % (alpha3, country, script))
def reverse(self, legendastv):
if legendastv in self.from_legendastv:
return self.from_legendastv[legendastv]
raise ConfigurationError('Unsupported language number for legendastv: %s' % legendastv)

View File

@ -591,7 +591,7 @@ def scan_videos(path, age=None, archives=True):
def refine(video, episode_refiners=None, movie_refiners=None, **kwargs):
"""Refine a video using :ref:`refiners`.
r"""Refine a video using :ref:`refiners`.
.. note::
@ -619,7 +619,7 @@ def refine(video, episode_refiners=None, movie_refiners=None, **kwargs):
def list_subtitles(videos, languages, pool_class=ProviderPool, **kwargs):
"""List subtitles.
r"""List subtitles.
The `videos` must pass the `languages` check of :func:`check_video`.
@ -660,7 +660,7 @@ def list_subtitles(videos, languages, pool_class=ProviderPool, **kwargs):
def download_subtitles(subtitles, pool_class=ProviderPool, **kwargs):
"""Download :attr:`~subliminal.subtitle.Subtitle.content` of `subtitles`.
r"""Download :attr:`~subliminal.subtitle.Subtitle.content` of `subtitles`.
:param subtitles: subtitles to download.
:type subtitles: list of :class:`~subliminal.subtitle.Subtitle`
@ -677,7 +677,7 @@ def download_subtitles(subtitles, pool_class=ProviderPool, **kwargs):
def download_best_subtitles(videos, languages, min_score=0, hearing_impaired=False, only_one=False, compute_score=None,
pool_class=ProviderPool, **kwargs):
"""List and download the best matching subtitles.
r"""List and download the best matching subtitles.
The `videos` must pass the `languages` and `undefined` (`only_one`) checks of :func:`check_video`.

Some files were not shown because too many files have changed in this diff Show More