mirror of
https://github.com/morpheus65535/bazarr.git
synced 2024-09-20 07:25:58 +08:00
Compare commits
8 commits
9bad2cf771
...
43667e276f
Author | SHA1 | Date | |
---|---|---|---|
43667e276f | |||
ff54bc83a9 | |||
dc384d7694 | |||
42781d5b68 | |||
b8aa2a8b1a | |||
6d062f3500 | |||
10d475e81e | |||
e4237a00b6 |
2
.github/workflows/schedule.yaml
vendored
2
.github/workflows/schedule.yaml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Execute
|
||||
uses: benc-uk/workflow-dispatch@v1.2.3
|
||||
uses: benc-uk/workflow-dispatch@v1.2.4
|
||||
with:
|
||||
workflow: "release_beta_to_dev"
|
||||
token: ${{ secrets.WF_GITHUB_TOKEN }}
|
||||
|
|
|
@ -282,6 +282,10 @@ validators = [
|
|||
Validator('legendasdivx.password', must_exist=True, default='', is_type_of=str, cast=str),
|
||||
Validator('legendasdivx.skip_wrong_fps', must_exist=True, default=False, is_type_of=bool),
|
||||
|
||||
# legendasnet section
|
||||
Validator('legendasnet.username', must_exist=True, default='', is_type_of=str, cast=str),
|
||||
Validator('legendasnet.password', must_exist=True, default='', is_type_of=str, cast=str),
|
||||
|
||||
# ktuvit section
|
||||
Validator('ktuvit.email', must_exist=True, default='', is_type_of=str),
|
||||
Validator('ktuvit.hashed_password', must_exist=True, default='', is_type_of=str, cast=str),
|
||||
|
|
|
@ -264,6 +264,10 @@ def get_providers_auth():
|
|||
'password': settings.legendasdivx.password,
|
||||
'skip_wrong_fps': settings.legendasdivx.skip_wrong_fps,
|
||||
},
|
||||
'legendasnet': {
|
||||
'username': settings.legendasnet.username,
|
||||
'password': settings.legendasnet.password,
|
||||
},
|
||||
'xsubs': {
|
||||
'username': settings.xsubs.username,
|
||||
'password': settings.xsubs.password,
|
||||
|
|
|
@ -5,7 +5,8 @@ import os
|
|||
|
||||
from subzero.language import Language
|
||||
|
||||
from app.database import database, insert
|
||||
from app.database import database, insert, update
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -18,7 +19,7 @@ class CustomLanguage:
|
|||
language = "pt-BR"
|
||||
official_alpha2 = "pt"
|
||||
official_alpha3 = "por"
|
||||
name = "Brazilian Portuguese"
|
||||
name = "Portuguese (Brazil)"
|
||||
iso = "BR"
|
||||
_scripts = []
|
||||
_possible_matches = ("pt-br", "pob", "pb", "brazilian", "brasil", "brazil")
|
||||
|
@ -50,13 +51,19 @@ class CustomLanguage:
|
|||
"""Register the custom language subclasses in the database."""
|
||||
|
||||
for sub in cls.__subclasses__():
|
||||
database.execute(
|
||||
insert(table)
|
||||
.values(code3=sub.alpha3,
|
||||
code2=sub.alpha2,
|
||||
name=sub.name,
|
||||
enabled=0)
|
||||
.on_conflict_do_nothing())
|
||||
try:
|
||||
database.execute(
|
||||
insert(table)
|
||||
.values(code3=sub.alpha3,
|
||||
code2=sub.alpha2,
|
||||
name=sub.name,
|
||||
enabled=0))
|
||||
except IntegrityError:
|
||||
database.execute(
|
||||
update(table)
|
||||
.values(code2=sub.alpha2,
|
||||
name=sub.name)
|
||||
.where(table.code3 == sub.alpha3))
|
||||
|
||||
@classmethod
|
||||
def found_external(cls, subtitle, subtitle_path):
|
||||
|
@ -212,7 +219,7 @@ class LatinAmericanSpanish(CustomLanguage):
|
|||
language = "es-MX"
|
||||
official_alpha2 = "es"
|
||||
official_alpha3 = "spa"
|
||||
name = "Latin American Spanish"
|
||||
name = "Spanish (Latino)"
|
||||
iso = "MX" # Not fair, but ok
|
||||
_scripts = ("419",)
|
||||
_possible_matches = (
|
||||
|
|
|
@ -44,6 +44,12 @@ def create_languages_dict():
|
|||
.values(name='Chinese Simplified')
|
||||
.where(TableSettingsLanguages.code3 == 'zho'))
|
||||
|
||||
# replace Modern Greek by Greek to match Sonarr and Radarr languages
|
||||
database.execute(
|
||||
update(TableSettingsLanguages)
|
||||
.values(name='Greek')
|
||||
.where(TableSettingsLanguages.code3 == 'ell'))
|
||||
|
||||
languages_dict = [{
|
||||
'code3': x.code3,
|
||||
'code2': x.code2,
|
||||
|
@ -54,13 +60,19 @@ def create_languages_dict():
|
|||
TableSettingsLanguages.code3b))
|
||||
.all()]
|
||||
|
||||
def audio_language_from_alpha2(lang):
|
||||
|
||||
def audio_language_from_name(lang):
|
||||
lang_map = {
|
||||
'Chinese': 'zh',
|
||||
'Portuguese (Brazil)': 'pb'
|
||||
}
|
||||
|
||||
return language_from_alpha2(lang_map.get(lang, lang))
|
||||
alpha2_code = lang_map.get(lang, None)
|
||||
|
||||
if alpha2_code is None:
|
||||
return lang
|
||||
|
||||
return language_from_alpha2(alpha2_code)
|
||||
|
||||
|
||||
def language_from_alpha2(lang):
|
||||
return next((item['name'] for item in languages_dict if item['code2'] == lang[:2]), None)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import os
|
||||
|
||||
from app.config import settings
|
||||
from languages.get_languages import audio_language_from_alpha2
|
||||
from languages.get_languages import audio_language_from_name
|
||||
from radarr.info import get_radarr_info
|
||||
from utilities.video_analyzer import embedded_audio_reader
|
||||
from utilities.path_mappings import path_mappings
|
||||
|
@ -117,7 +117,7 @@ def movieParser(movie, action, tags_dict, language_profiles, movie_default_profi
|
|||
for item in movie['movieFile']['languages']:
|
||||
if isinstance(item, dict):
|
||||
if 'name' in item:
|
||||
language = audio_language_from_alpha2(item['name'])
|
||||
language = audio_language_from_name(item['name'])
|
||||
audio_language.append(language)
|
||||
|
||||
tags = [d['label'] for d in tags_dict if d['id'] in movie['tags']]
|
||||
|
|
|
@ -5,7 +5,7 @@ import os
|
|||
from app.config import settings
|
||||
from app.database import TableShows, database, select
|
||||
from constants import MINIMUM_VIDEO_SIZE
|
||||
from languages.get_languages import audio_language_from_alpha2
|
||||
from languages.get_languages import audio_language_from_name
|
||||
from utilities.path_mappings import path_mappings
|
||||
from utilities.video_analyzer import embedded_audio_reader
|
||||
from sonarr.info import get_sonarr_info
|
||||
|
@ -122,13 +122,13 @@ def episodeParser(episode):
|
|||
item = episode['episodeFile']['language']
|
||||
if isinstance(item, dict):
|
||||
if 'name' in item:
|
||||
audio_language.append(audio_language_from_alpha2(item['name']))
|
||||
audio_language.append(audio_language_from_name(item['name']))
|
||||
elif 'languages' in episode['episodeFile'] and len(episode['episodeFile']['languages']):
|
||||
items = episode['episodeFile']['languages']
|
||||
if isinstance(items, list):
|
||||
for item in items:
|
||||
if 'name' in item:
|
||||
audio_language.append(audio_language_from_alpha2(item['name']))
|
||||
audio_language.append(audio_language_from_name(item['name']))
|
||||
else:
|
||||
audio_language = database.execute(
|
||||
select(TableShows.audio_language)
|
||||
|
|
264
custom_libs/subliminal_patch/providers/legendasnet.py
Normal file
264
custom_libs/subliminal_patch/providers/legendasnet.py
Normal file
|
@ -0,0 +1,264 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import io
|
||||
import json
|
||||
|
||||
from zipfile import ZipFile, is_zipfile
|
||||
from urllib.parse import urljoin
|
||||
from requests import Session
|
||||
|
||||
from subzero.language import Language
|
||||
from subliminal import Episode, Movie
|
||||
from subliminal.exceptions import ConfigurationError, ProviderError, DownloadLimitExceeded
|
||||
from subliminal_patch.exceptions import APIThrottled
|
||||
from .mixins import ProviderRetryMixin
|
||||
from subliminal_patch.subtitle import Subtitle
|
||||
from subliminal.subtitle import fix_line_ending
|
||||
from subliminal_patch.providers import Provider
|
||||
from subliminal_patch.providers import utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
retry_amount = 3
|
||||
retry_timeout = 5
|
||||
|
||||
|
||||
class LegendasNetSubtitle(Subtitle):
|
||||
provider_name = 'legendasnet'
|
||||
hash_verifiable = False
|
||||
|
||||
def __init__(self, language, forced, page_link, download_link, file_id, release_names, uploader,
|
||||
season=None, episode=None):
|
||||
super().__init__(language)
|
||||
language = Language.rebuild(language, forced=forced)
|
||||
|
||||
self.season = season
|
||||
self.episode = episode
|
||||
self.releases = release_names
|
||||
self.release_info = ', '.join(release_names)
|
||||
self.language = language
|
||||
self.forced = forced
|
||||
self.file_id = file_id
|
||||
self.page_link = page_link
|
||||
self.download_link = download_link
|
||||
self.uploader = uploader
|
||||
self.matches = None
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.file_id
|
||||
|
||||
def get_matches(self, video):
|
||||
matches = set()
|
||||
|
||||
# handle movies and series separately
|
||||
if isinstance(video, Episode):
|
||||
# series
|
||||
matches.add('series')
|
||||
# season
|
||||
if video.season == self.season:
|
||||
matches.add('season')
|
||||
# episode
|
||||
if video.episode == self.episode:
|
||||
matches.add('episode')
|
||||
# imdb
|
||||
matches.add('series_imdb_id')
|
||||
else:
|
||||
# title
|
||||
matches.add('title')
|
||||
# imdb
|
||||
matches.add('imdb_id')
|
||||
|
||||
utils.update_matches(matches, video, self.release_info)
|
||||
|
||||
self.matches = matches
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
class LegendasNetProvider(ProviderRetryMixin, Provider):
|
||||
"""Legendas.Net Provider"""
|
||||
server_hostname = 'legendas.net/api'
|
||||
|
||||
languages = {Language('por', 'BR')}
|
||||
video_types = (Episode, Movie)
|
||||
|
||||
def __init__(self, username, password):
|
||||
self.session = Session()
|
||||
self.session.headers = {'User-Agent': os.environ.get("SZ_USER_AGENT", "Sub-Zero/2")}
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.access_token = None
|
||||
self.video = None
|
||||
self._started = None
|
||||
self.login()
|
||||
|
||||
def login(self):
|
||||
headersList = {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": self.session.headers['User-Agent'],
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
payload = json.dumps({
|
||||
"email": self.username,
|
||||
"password": self.password
|
||||
})
|
||||
|
||||
response = self.session.request("POST", self.server_url() + 'login', data=payload, headers=headersList)
|
||||
if response.status_code != 200:
|
||||
raise ConfigurationError('Failed to login and retrieve access token')
|
||||
self.access_token = response.json().get('access_token')
|
||||
if not self.access_token:
|
||||
raise ConfigurationError('Access token not found in login response')
|
||||
self.session.headers.update({'Authorization': f'Bearer {self.access_token}'})
|
||||
|
||||
def initialize(self):
|
||||
self._started = time.time()
|
||||
|
||||
def terminate(self):
|
||||
self.session.close()
|
||||
|
||||
def server_url(self):
|
||||
return f'https://{self.server_hostname}/v1/'
|
||||
|
||||
def query(self, languages, video):
|
||||
self.video = video
|
||||
|
||||
# query the server
|
||||
if isinstance(self.video, Episode):
|
||||
res = self.retry(
|
||||
lambda: self.session.get(self.server_url() + 'search/tv',
|
||||
json={
|
||||
'name': video.series,
|
||||
'page': 1,
|
||||
'per_page': 25,
|
||||
'tv_episode': video.episode,
|
||||
'tv_season': video.season,
|
||||
'imdb_id': video.series_imdb_id
|
||||
},
|
||||
headers={'Content-Type': 'application/json'},
|
||||
timeout=30),
|
||||
amount=retry_amount,
|
||||
retry_timeout=retry_timeout
|
||||
)
|
||||
else:
|
||||
res = self.retry(
|
||||
lambda: self.session.get(self.server_url() + 'search/movie',
|
||||
json={
|
||||
'name': video.title,
|
||||
'page': 1,
|
||||
'per_page': 25,
|
||||
'imdb_id': video.imdb_id
|
||||
},
|
||||
headers={'Content-Type': 'application/json'},
|
||||
timeout=30),
|
||||
amount=retry_amount,
|
||||
retry_timeout=retry_timeout
|
||||
)
|
||||
|
||||
if res.status_code == 404:
|
||||
logger.error(f"Endpoint not found: {res.url}")
|
||||
raise ProviderError("Endpoint not found")
|
||||
elif res.status_code == 429:
|
||||
raise APIThrottled("Too many requests")
|
||||
elif res.status_code == 403:
|
||||
raise ConfigurationError("Invalid access token")
|
||||
elif res.status_code != 200:
|
||||
res.raise_for_status()
|
||||
|
||||
subtitles = []
|
||||
|
||||
result = res.json()
|
||||
|
||||
if ('success' in result and not result['success']) or ('status' in result and not result['status']):
|
||||
logger.debug(result["error"])
|
||||
return []
|
||||
|
||||
if isinstance(self.video, Episode):
|
||||
if len(result['tv_shows']):
|
||||
for item in result['tv_shows']:
|
||||
subtitle = LegendasNetSubtitle(
|
||||
language=Language('por', 'BR'),
|
||||
forced=self._is_forced(item),
|
||||
page_link=f"https://legendas.net/tv_legenda?movie_id={result['tv_shows'][0]['tmdb_id']}&"
|
||||
f"legenda_id={item['id']}",
|
||||
download_link=item['path'],
|
||||
file_id=item['id'],
|
||||
release_names=[item.get('release_name', '')],
|
||||
uploader=item['uploader'],
|
||||
season=item.get('season', ''),
|
||||
episode=item.get('episode', '')
|
||||
)
|
||||
subtitle.get_matches(self.video)
|
||||
if subtitle.language in languages:
|
||||
subtitles.append(subtitle)
|
||||
else:
|
||||
if len(result['movies']):
|
||||
for item in result['movies']:
|
||||
subtitle = LegendasNetSubtitle(
|
||||
language=Language('por', 'BR'),
|
||||
forced=self._is_forced(item),
|
||||
page_link=f"https://legendas.net/legenda?movie_id={result['movies'][0]['tmdb_id']}&"
|
||||
f"legenda_id={item['id']}",
|
||||
download_link=item['path'],
|
||||
file_id=item['id'],
|
||||
release_names=[item.get('release_name', '')],
|
||||
uploader=item['uploader'],
|
||||
season=None,
|
||||
episode=None
|
||||
)
|
||||
subtitle.get_matches(self.video)
|
||||
if subtitle.language in languages:
|
||||
subtitles.append(subtitle)
|
||||
|
||||
return subtitles
|
||||
|
||||
@staticmethod
|
||||
def _is_forced(item):
|
||||
forced_tags = ['forced', 'foreign']
|
||||
for tag in forced_tags:
|
||||
if tag in item.get('comment', '').lower():
|
||||
return True
|
||||
|
||||
# nothing match so we consider it as normal subtitles
|
||||
return False
|
||||
|
||||
def list_subtitles(self, video, languages):
|
||||
return self.query(languages, video)
|
||||
|
||||
def download_subtitle(self, subtitle):
|
||||
logger.debug('Downloading subtitle %r', subtitle)
|
||||
download_link = urljoin("https://legendas.net", subtitle.download_link)
|
||||
|
||||
r = self.retry(
|
||||
lambda: self.session.get(download_link, timeout=30),
|
||||
amount=retry_amount,
|
||||
retry_timeout=retry_timeout
|
||||
)
|
||||
|
||||
if r.status_code == 429:
|
||||
raise DownloadLimitExceeded("Daily download limit exceeded")
|
||||
elif r.status_code == 403:
|
||||
raise ConfigurationError("Invalid access token")
|
||||
elif r.status_code != 200:
|
||||
r.raise_for_status()
|
||||
|
||||
if not r:
|
||||
logger.error(f'Could not download subtitle from {download_link}')
|
||||
subtitle.content = None
|
||||
return
|
||||
else:
|
||||
archive_stream = io.BytesIO(r.content)
|
||||
if is_zipfile(archive_stream):
|
||||
archive = ZipFile(archive_stream)
|
||||
for name in archive.namelist():
|
||||
subtitle_content = archive.read(name)
|
||||
subtitle.content = fix_line_ending(subtitle_content)
|
||||
return
|
||||
else:
|
||||
subtitle_content = r.content
|
||||
subtitle.content = fix_line_ending(subtitle_content)
|
||||
return
|
8
frontend/package-lock.json
generated
8
frontend/package-lock.json
generated
|
@ -38,7 +38,7 @@
|
|||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.1",
|
||||
"@types/node": "^20.12.6",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||
"@typescript-eslint/parser": "^7.16.0",
|
||||
|
@ -3481,9 +3481,9 @@
|
|||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz",
|
||||
"integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==",
|
||||
"version": "18.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz",
|
||||
"integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.1",
|
||||
"@types/node": "^20.12.6",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||
"@typescript-eslint/parser": "^7.16.0",
|
||||
|
|
|
@ -305,6 +305,21 @@ export const ProviderList: Readonly<ProviderInfo[]> = [
|
|||
{ type: "switch", key: "skip_wrong_fps", name: "Skip Wrong FPS" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "legendasnet",
|
||||
name: "Legendas.net",
|
||||
description: "Brazilian Subtitles Provider",
|
||||
inputs: [
|
||||
{
|
||||
type: "text",
|
||||
key: "username",
|
||||
},
|
||||
{
|
||||
type: "password",
|
||||
key: "password",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ key: "napiprojekt", description: "Polish Subtitles Provider" },
|
||||
{
|
||||
key: "napisy24",
|
||||
|
|
Loading…
Reference in a new issue