├── bin ├── readme.md ├── Tvdb.dll └── MovieDb.dll ├── requirements.txt ├── genre_mapper ├── readme.md └── genre_mapper.py ├── delete_episode_genre ├── readme.md └── delete_episode_genre.py ├── theme_song_scraper ├── readme.md ├── spider │ └── lizardbyte_spider.py └── theme_song_scraper.py ├── alternative_renamer ├── readme.md └── alternative_renamer.py ├── season_renamer ├── readme.md └── season_renamer.py ├── Dockerfile ├── .github └── workflows │ ├── scrap_theme.yml │ └── docker-image.yml ├── compose.yml ├── README.md ├── country_scraper ├── readme.md └── country_scraper.py ├── docker_entrance.py └── strm_mediainfo └── strm_mediainfo.py /bin/readme.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /bin/Tvdb.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuroyukihime0/emby_scripts/HEAD/bin/Tvdb.dll -------------------------------------------------------------------------------- /bin/MovieDb.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuroyukihime0/emby_scripts/HEAD/bin/MovieDb.dll -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | python-dateutil 3 | opencc-python-reimplemented 4 | yt-dlp 5 | -------------------------------------------------------------------------------- /genre_mapper/readme.md: -------------------------------------------------------------------------------- 1 | ## genre_mapper 2 | 3 | 批量替换或者移除Emby电影/剧集的genre 4 | 需要填写 Emby服务器地址, API 密钥, USER_ID, 需要修改的库名 5 | -------------------------------------------------------------------------------- /delete_episode_genre/readme.md: -------------------------------------------------------------------------------- 1 | ## delete_episode_genre 2 | 3 | 批量清Emby剧集的单集genre 4 | 需要填写 Emby服务器地址, API 密钥, USER_ID, 需要修改的库名 5 | -------------------------------------------------------------------------------- /theme_song_scraper/readme.md: -------------------------------------------------------------------------------- 1 | ## theme_song_scraper 2 | 3 | 刮削电影/剧集主题曲 4 | 刮削电影主题曲需要pip install yt-dlp 或是安装根目录requirement.txt 5 | -------------------------------------------------------------------------------- /alternative_renamer/readme.md: -------------------------------------------------------------------------------- 1 | ## alternative_renamer 2 | 3 | 批量增加电影和剧集的别名到SortName, 帮助检索(*建议搭配小秘搜索补丁食用) 4 | 需要填写 Emby服务器地址, API 密钥, USER_ID, 需要修改的库名,TMDB API KEY, 具体以文件开头注释为准 5 | 6 | 效果预览: 7 | ![image](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/8cf723a1-cfbb-4d2e-a363-7f82c82fa4f1) 8 | ![image](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/473c44d7-d772-4369-8557-e504281ba72e) 9 | 10 | 11 | -------------------------------------------------------------------------------- /season_renamer/readme.md: -------------------------------------------------------------------------------- 1 | ## season_renamer 2 | 3 | 批量刮削TMDB季名 4 | 可以直接使用alternative_renamer.py的缓存来刮削*(tmdb.json) 5 | 需要填写 Emby服务器地址, API 密钥, USER_ID, 需要修改的库名,TMDB API KEY, 具体以文件开头注释为准 6 | 7 | 效果预览: 8 | ![image](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/79ecf468-ecf3-48bc-9f53-263d06db91fc) 9 | ![image](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/6f238382-0766-4f6b-8c3e-7135a28c18ae) 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine 2 | 3 | WORKDIR /app 4 | COPY requirements.txt /app/ 5 | RUN pip install -r requirements.txt 6 | 7 | ENV EMBY_HOST= 8 | ENV EMBY_API_KEY= 9 | ENV EMBY_USER_ID= 10 | ENV TMDB_KEY= 11 | ENV LIB_NAME= 12 | ENV DRY_RUN=true 13 | ENV RUN_INTERVAL_HOURS=24 14 | 15 | ENV ENABLE_ALTERNATIVE_RENAMER=true 16 | ENV ENABLE_COUNTRY_SCAPTER=true 17 | ENV ENABLE_GENRE_MAPPER=true 18 | ENV ENABLE_SEASON_RENAMER=true 19 | 20 | COPY . /app/ 21 | CMD [ "python", "/app/docker_entrance.py" ] 22 | -------------------------------------------------------------------------------- /.github/workflows/scrap_theme.yml: -------------------------------------------------------------------------------- 1 | name: Scrape Movie Theme 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout from repo 10 | uses: actions/checkout@main 11 | with: 12 | ref: master 13 | - name: Install Python latest 14 | uses: actions/setup-python@main 15 | with: 16 | python-version: '3.x' 17 | architecture: 'x64' 18 | - name: Install dependencies 19 | run: | 20 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 21 | - name: Doing fetch 22 | run: python theme_song_scraper/spider/lizardbyte_spider.py 23 | - name: Commit files 24 | run: | 25 | git config --local user.email "a@b.c" 26 | git config --local user.name "Joker" 27 | git add . 28 | git commit -m "update theme song database" 29 | - name: Push changes 30 | uses: ad-m/github-push-action@master 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | branch: master 34 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | emby_script: 4 | container_name: emby_scripts_3888 5 | image: hush114514/emby_scripts 6 | network_mode: bridge 7 | # OPTIONAL: 挂载tmdb缓存到容器外 8 | # volumes: 9 | # - /your_localpath/tmdb_alt_name.json:/app/tmdb_alt_name.json 10 | # - /your_localpath/country.json:/app/country.json 11 | # - /your_localpath/tmdb.json:/app/tmdb.json 12 | environment: 13 | - TZ=Asia/Shanghai 14 | # EMBY地址 15 | - EMBY_HOST= http://xxx:8092 16 | # EMBY的API_KEY 17 | - EMBY_API_KEY= 18 | # EMBY的USERID 19 | - EMBY_USER_ID= 20 | # tmdb API 读访问令牌 21 | - TMDB_KEY= 22 | # 库名 多个时英文逗号分隔 23 | - LIB_NAME= 24 | # DRY_RUN = true时为预览, false实际运行 25 | - DRY_RUN=true 26 | # 运行间隔 27 | - RUN_INTERVAL_HOURS=24 28 | # 是否增加港台标题 29 | - ADD_HANT_TITLE=true 30 | # 是否启用各个模块 31 | - ENABLE_ALTERNATIVE_RENAMER=true 32 | - ENABLE_COUNTRY_SCAPTER=true 33 | - ENABLE_GENRE_MAPPER=true 34 | - ENABLE_SEASON_RENAMER=true 35 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker Image 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | env: 8 | APP_NAME: emby_scripts 9 | DOCKERHUB_REPO: hush114514/emby_scripts 10 | 11 | jobs: 12 | main: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Set up QEMU 18 | uses: docker/setup-qemu-action@v1 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v1 21 | - name: Login to DockerHub 22 | uses: docker/login-action@v1 23 | with: 24 | username: ${{ secrets.DOCKERHUB_USERNAME }} 25 | password: ${{ secrets.DOCKERHUB_TOKEN }} 26 | - name: Generate App Version 27 | run: echo APP_VERSION=`git describe --tags --always` >> $GITHUB_ENV 28 | - name: Build and push 29 | id: docker_build 30 | uses: docker/build-push-action@v2 31 | with: 32 | push: true 33 | platforms: | 34 | linux/amd64 35 | linux/arm64/v8 36 | build-args: | 37 | APP_NAME=${{ env.APP_NAME }} 38 | APP_VERSION=${{ env.APP_VERSION }} 39 | tags: | 40 | ${{ env.DOCKERHUB_REPO }}:${{ env.APP_VERSION }} 41 | ${{ env.DOCKERHUB_REPO }}:latest 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 一些Emby的实用小脚本 2 | 3 | [alternative_renamer](https://github.com/kuroyukihime0/emby-scripts/tree/master/alternative_renamer) -> 刮削别名方便搜索, 推荐搭配小秘搜索补丁精简版使用 4 | [delete_episode_genre](https://github.com/kuroyukihime0/emby-scripts/tree/master/delete_episode_genre) -> 删除所有剧集单集的genre 5 | [genre_mapper](https://github.com/kuroyukihime0/emby-scripts/tree/master/genre_mapper) -> 批量删除或者替换特定的genre, 如'Sci-Fi & Fantasy':'科幻 6 | [season_renamer](https://github.com/kuroyukihime0/emby-scripts/tree/master/season_renamer) -> 刮削剧集的季名 7 | [country_scraper](https://github.com/kuroyukihime0/emby-scripts/tree/master/country_scraper) -> 刮削电影/剧集的国家/语言作为标签(tag) 8 | [theme_song_scraper](https://github.com/kuroyukihime0/emby_scripts/tree/master/theme_song_scraper) -> 刮削剧集/电影主题曲 9 | 10 | [strm_mediainfo](https://github.com/kuroyukihime0/emby_scripts/tree/master/strm_mediainfo) -> 强制生成strm文件的mediainfo 11 | 12 | 具体以脚本readme为准 13 | 14 | 15 | 16 | ## 部署 17 | 本地: pip install -r requirements.txt 后, 分别执行子目录下python文件 18 | Docker部署: 使用[Docker Compose](https://github.com/kuroyukihime0/emby-scripts/blob/master/compose.yml) 参考注释配置环境变量 19 | 20 | ## 其他 21 | [修改版TMDB插件](https://github.com/kuroyukihime0/emby_scripts/blob/master/bin/MovieDb.dll) 22 | [修改版TVDB插件](https://github.com/kuroyukihime0/emby_scripts/blob/master/bin/Tvdb.dll) 23 | 版本号为10.X, 移除无默认语言时刮削其他语言, TMDB插件支持zh-sg作为标题备选语言(简中标题被锁时可以刮到zh-sg) 24 | 25 | ChangeLog: 26 | 2024.05.23: Tmdb插件支持修复无法刮到中文集截图 27 | 2024.05.11: Tmdb插件支持刮削季名 28 | -------------------------------------------------------------------------------- /theme_song_scraper/spider/lizardbyte_spider.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import requests 4 | import json 5 | 6 | session = requests.Session() 7 | current_dir = os.path.abspath(os.path.dirname(__file__)) 8 | json_file = os.path.join(current_dir,'lizardbyte.json') 9 | 10 | try: 11 | f = open(json_file, 'rb') 12 | content = f.read() 13 | items = json.loads(content) 14 | except Exception as ex: 15 | items = [] 16 | 17 | 18 | 19 | def save_to_json(): 20 | f2 = open(json_file, 'w') 21 | f2.write(json.dumps(items)) 22 | f2.close() 23 | 24 | 25 | 26 | def get_json(url,retry = 0): 27 | try: 28 | response = session.get(url) 29 | if response.status_code == 200: 30 | resp_json = response.json() 31 | return resp_json 32 | else: 33 | return None 34 | except Exception as ex: 35 | print(str(ex)) 36 | if(retry <=3): 37 | return get_json(url,retry +1) 38 | 39 | def get_page_count(): 40 | return get_json("https://app.lizardbyte.dev/ThemerrDB/movies/pages.json") 41 | 42 | def get_page_detail(page): 43 | return get_json( f"https://app.lizardbyte.dev/ThemerrDB/movies/all_page_{page}.json") 44 | 45 | def get_item_theme(url): 46 | resp_json = get_json(url) 47 | if not resp_json or "youtube_theme_url" not in resp_json: 48 | return None 49 | return resp_json["youtube_theme_url"] 50 | 51 | page_count = get_page_count() 52 | if page_count: 53 | page_all = page_count['pages'] 54 | for page in range(1,page_all +1): 55 | resp = get_page_detail(page) 56 | for item in resp: 57 | if item in items: 58 | pass 59 | else: 60 | url = None 61 | if 'id' in item: 62 | url = f"https://app.lizardbyte.dev/ThemerrDB/movies/themoviedb/{item['id']}.json" 63 | elif 'imdb_id' in item: 64 | url = f"https://app.lizardbyte.dev/ThemerrDB/movies/imdb/{item['imdb_id']}.json" 65 | if url: 66 | item['theme'] = get_item_theme(url) 67 | items.append(item) 68 | save_to_json() 69 | else: 70 | pass 71 | print('all done') 72 | -------------------------------------------------------------------------------- /country_scraper/readme.md: -------------------------------------------------------------------------------- 1 | ## country_scraper 2 | 3 | 刮削电影/剧集的国家/语言作为标签(tag) 4 | 5 | 效果预览: 6 | ![image](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/68469f85-21e8-4cde-804e-e057324e2e15) 7 | ![image](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/f0ff61ab-42dd-4123-940a-1bd46e4bb73d) 8 | 9 | 10 | 标签封面: 11 | ![加拿大](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/7a784141-a330-40d3-b113-1a9491b8d084) 12 | ![韩语](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/a9762748-55c9-4488-8660-8f46718c2a5e) 13 | ![韩国](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/9268ec74-274b-4b0a-beea-0b5e049c17f3) 14 | ![国语](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/305fe0d7-e077-4757-a294-076f1dc5fac6) 15 | ![粤语](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/af56a749-8c9f-48cc-b2ec-fdd31caccea7) 16 | ![英语](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/663c5200-9151-4848-993c-29d3e53f2c7e) 17 | ![英国](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/6e1b2ce5-30e9-4ea1-a7b6-72a9deee2146) 18 | ![印度](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/d4139ea1-a472-4ce6-9653-9fbfd37f514c) 19 | ![香港](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/2f1ed9f5-b129-42ea-801b-40525bb5c383) 20 | ![西班牙语](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/d5674ce3-4f26-4d4e-b2f3-99a01f681b42) 21 | ![台湾](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/4b06f987-678e-4029-acc7-278a1f2c13f2) 22 | ![日语](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/3a9f5e4e-dc22-47ee-aa2c-003df0f435e8) 23 | ![日本](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/a0cf96bc-25b4-4ec7-9189-769b00675476) 24 | ![其他语种](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/2c544a76-286f-4d8d-9713-6cd62d72264c) 25 | ![其他国家](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/dc9aa443-d45b-4088-96b7-5ae2cc973e9b) 26 | ![其他](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/fa6c8bc0-885f-4dc0-9879-412a0d2a1543) 27 | ![美国](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/0a8a370d-ddb2-4abc-aba0-adad975708d4) 28 | ![国产](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/3f32d904-4dcc-4c1f-bc28-a1f7df49bab9) 29 | ![法语](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/eea57791-1b16-4ba8-b26e-6c5243ac921b) 30 | ![法国](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/1fd3540a-e097-4861-acc0-bd48d217cc82) 31 | ![俄语](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/c5da09f7-ad83-4c1f-b25c-2c1330f9098e) 32 | ![俄罗斯](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/4fe82be6-9043-4a0d-8e80-78e25feb7b15) 33 | ![德语](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/c545d03a-5496-4354-acee-b0a64ee37b32) 34 | ![德国](https://github.com/kuroyukihime0/emby-scripts/assets/7975549/63b54fbe-da40-4c6b-ab9f-5c60f7c40595) 35 | 36 | 37 | -------------------------------------------------------------------------------- /docker_entrance.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | import logging 5 | from alternative_renamer import alternative_renamer 6 | from country_scraper import country_scraper 7 | from genre_mapper import genre_mapper 8 | from season_renamer import season_renamer 9 | 10 | ENV_RUN_INTERVAL_HOURS = int(os.environ['RUN_INTERVAL_HOURS']) 11 | ENV_ENABLE_ALTERNATIVE_RENAMER = (os.getenv('ENABLE_ALTERNATIVE_RENAMER') in['True','true']) 12 | ENV_ENABLE_COUNTRY_SCAPTER = (os.getenv('ENABLE_COUNTRY_SCAPTER') in['True','true']) 13 | ENV_ENABLE_GENRE_MAPPER = (os.getenv('ENABLE_GENRE_MAPPER') in['True','true']) 14 | ENV_ENABLE_SEASON_RENAMER = (os.getenv('ENABLE_SEASON_RENAMER') in['True','true']) 15 | 16 | 17 | ENV_EMBY_HOST = os.environ["EMBY_HOST"] 18 | ENV_EMBY_API_KEY = os.environ["EMBY_API_KEY"] 19 | ENV_EMBY_USER_ID = os.environ["EMBY_USER_ID"] 20 | ENV_TMDB_KEY = os.environ["TMDB_KEY"] 21 | ENV_LIB_NAME = os.environ["LIB_NAME"] 22 | ENV_DRY_RUN = (os.getenv('DRY_RUN') in['True','true']) 23 | ENV_ADD_HANT_TITLE = (os.getenv('ADD_HANT_TITLE') in['True','true']) 24 | 25 | log = logging.getLogger('entrance') 26 | log.setLevel(logging.DEBUG) 27 | formatter = logging.Formatter( 28 | '%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') 29 | fh = logging.FileHandler('logs.log', encoding='utf-8') 30 | fh.setLevel(logging.DEBUG) 31 | fh.setFormatter(formatter) 32 | ch = logging.StreamHandler() 33 | ch.setLevel(logging.DEBUG) 34 | ch.setFormatter(formatter) 35 | log.addHandler(ch) 36 | log.addHandler(fh) 37 | 38 | 39 | def get_or_default(value, default=None): 40 | return value if value else default 41 | 42 | def work(): 43 | try: 44 | if ENV_ENABLE_ALTERNATIVE_RENAMER: 45 | log.info('START ALTERNATIVE_RENAMER') 46 | alternative_renamer.run_renameer() 47 | else: 48 | log.info('SKIP ALTERNATIVE_RENAMER') 49 | if ENV_ENABLE_SEASON_RENAMER: 50 | log.info('START SEASON_RENAMER') 51 | season_renamer.run_renamer() 52 | else: 53 | log.info('SKIP SEASON_RENAMER') 54 | if ENV_ENABLE_COUNTRY_SCAPTER: 55 | log.info('START COUNTRY_SCAPTER') 56 | country_scraper.run_scraper() 57 | else: 58 | log.info('SKIP COUNTRY_SCAPTER') 59 | if ENV_ENABLE_GENRE_MAPPER: 60 | log.info('START GENRE_MAPPER') 61 | genre_mapper.run_mapper() 62 | else: 63 | log.info('SKIP GENRE_MAPPER') 64 | except Exception as ex: 65 | log.error(str(ex)) 66 | 67 | 68 | def work_loop(): 69 | while True: 70 | work() 71 | interval_hour = ENV_RUN_INTERVAL_HOURS if ENV_RUN_INTERVAL_HOURS else 24 72 | time.sleep(interval_hour * 3600) 73 | 74 | 75 | if __name__ == "__main__": 76 | modules = [alternative_renamer, country_scraper, 77 | genre_mapper, season_renamer] 78 | assert ENV_EMBY_HOST 79 | assert ENV_EMBY_API_KEY 80 | assert ENV_EMBY_USER_ID 81 | assert ENV_LIB_NAME 82 | 83 | for module in modules: 84 | config = module.config 85 | 86 | config['EMBY_SERVER'] = ENV_EMBY_HOST if ENV_EMBY_HOST else '' 87 | config['API_KEY'] = ENV_EMBY_API_KEY if ENV_EMBY_API_KEY else '' 88 | config['USER_ID'] = ENV_EMBY_USER_ID if ENV_EMBY_USER_ID else '' 89 | config['TMDB_KEY'] = ENV_TMDB_KEY if ENV_TMDB_KEY else '' 90 | config['LIB_NAME'] = ENV_LIB_NAME if ENV_LIB_NAME else '' 91 | config['DRY_RUN'] = ENV_DRY_RUN 92 | config['ADD_HANT_TITLE'] = ENV_ADD_HANT_TITLE 93 | config['IS_DOCKER'] = True 94 | 95 | work_loop() 96 | -------------------------------------------------------------------------------- /strm_mediainfo/strm_mediainfo.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import time 4 | 5 | # 设置 Emby 服务器地址和 API 密钥 6 | EMBY_SERVER = 'http://xxx:8096' # 根据您的 Emby 服务器地址修改 7 | # API 密钥 8 | API_KEY = '' 9 | USER_ID = '' 10 | # 库名 英文逗号分隔 11 | LIB_NAME = '' 12 | # True 时为测试, False 实际写入 13 | DRY_RUN = True 14 | # 扫描延迟 15 | DELAY = 10 16 | 17 | 18 | process_count = 0 19 | headers = { 20 | 'X-Emby-Token': API_KEY, 21 | 'Content-Type': 'application/json', 22 | } 23 | 24 | session = requests.session() 25 | 26 | 27 | def playbackinfo(item_id, name): 28 | global process_count 29 | resp = session.post( 30 | f'{EMBY_SERVER}/Items/{item_id}/PlaybackInfo?AutoOpenLiveStream=true&IsPlayback=true&api_key={API_KEY}&UserId={USER_ID}', headers=headers) 31 | if resp.status_code == 200: 32 | process_count+1 33 | print(f' {name} success') 34 | else: 35 | print(f' {name} error') 36 | time.sleep(DELAY) 37 | 38 | 39 | def process_item(item_id, name): 40 | resp = session.get( 41 | f'{EMBY_SERVER}/emby/Users/{USER_ID}/Items/{item_id}?Fields=ChannelMappingInfo&api_key={API_KEY}', headers=headers) 42 | item = resp.json() 43 | if 'MediaStreams' in item: 44 | if 'LocationType' in item and item['LocationType'] == 'Virtual': 45 | return 46 | if len(item['MediaStreams']) == 0: 47 | print(f"** 开始处理{name}") 48 | if not DRY_RUN: 49 | playbackinfo(item_id, name) 50 | 51 | 52 | def process_series(parent_id): 53 | params = {'ParentId': parent_id} 54 | response = session.get(f'{EMBY_SERVER}/emby/Items', 55 | headers=headers, params=params) 56 | 57 | seasons = response.json()['Items'] 58 | for seasons in seasons: 59 | seaeson_id = seasons['Id'] 60 | season_name = seasons['Name'] 61 | series_name = seasons['SeriesName'] 62 | params = { 63 | 'ParentId': seaeson_id, 64 | 'IncludeItemTypes': 'Episode', 65 | 'Recursive': 'true', 66 | 'SortBy': 'SortName', 67 | 'SortOrder': 'Ascending' 68 | } 69 | epoisode_response = session.get( 70 | f'{EMBY_SERVER}/emby/Items', headers=headers, params=params) 71 | episodes = epoisode_response.json()['Items'] 72 | for episode in episodes: 73 | episode_id = episode['Id'] 74 | episode_name = episode['Name'] 75 | process_item( 76 | episode_id, f'{series_name} {season_name} {episode_name}') 77 | 78 | 79 | def get_library_id(name): 80 | if not name: 81 | return 82 | res = session.get( 83 | f'{EMBY_SERVER}/emby/Library/VirtualFolders', headers=headers) 84 | lib_id = [i['ItemId'] for i in res.json() if i['Name'] == name] 85 | if not lib_id: 86 | raise KeyError(f'library: {name} not exists, check it') 87 | return lib_id[0] if lib_id else None 88 | 89 | 90 | def get_lib_items(parent_id): 91 | params = {'ParentId': parent_id, 92 | # 'HasTmdbId': True, 93 | 'fields': 'ProviderIds' 94 | } 95 | response = session.get(f'{EMBY_SERVER}/emby/Items', 96 | headers=headers, params=params) 97 | items = response.json()['Items'] 98 | items_folder = [item for item in items if item["Type"] == "Folder"] 99 | items = [item for item in items if item["Type"] != "Folder"] 100 | for folder in items_folder: 101 | items = items + get_lib_items(folder['Id']) 102 | 103 | return items 104 | 105 | 106 | if __name__ == '__main__': 107 | libs = LIB_NAME.split(',') 108 | for lib_name in libs: 109 | parent_id = get_library_id(lib_name.strip()) 110 | series = get_lib_items(parent_id) 111 | print(f'**库 {lib_name} 中共有{len(series)} 个item,开始处理') 112 | for serie in series: 113 | serie_id = serie['Id'] 114 | name = serie['Name'] 115 | type = serie['Type'] 116 | if type == 'Movie': 117 | process_item(serie_id, name) 118 | elif type == 'Series': 119 | process_series(serie_id) 120 | 121 | print(f'**更新成功{process_count}条') 122 | -------------------------------------------------------------------------------- /delete_episode_genre/delete_episode_genre.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | # 设置 Emby 服务器地址和 API 密钥 5 | EMBY_SERVER = 'http://xxx:8096' 6 | # 设置 API 密钥 7 | API_KEY = '' 8 | # 设置 USERID 9 | USER_ID = '' 10 | 11 | # 设置库名, 多个时用,隔开(只支持剧集库, 填电影库后果自负) 12 | LIB_NAME = 'A,B' 13 | # True 时为测试, False 实际写入 14 | DRY_RUN = True 15 | 16 | 17 | headers = { 18 | 'X-Emby-Token': API_KEY, 19 | 'Content-Type': 'application/json', 20 | } 21 | 22 | session = requests.session() 23 | 24 | process_count = 0 25 | 26 | 27 | def remove_genre_for_episodes(parent_id): 28 | global process_count 29 | # 获取剧集列表 30 | params = {'ParentId': parent_id} 31 | response = session.get(f'{EMBY_SERVER}/emby/Items', 32 | headers=headers, params=params) 33 | 34 | seasons = response.json()['Items'] 35 | for seasons in seasons: 36 | seaeson_id = seasons['Id'] 37 | season_name = seasons['Name'] 38 | series_name = seasons['SeriesName'] 39 | # 获取分集ID 40 | params = { 41 | 'ParentId': seaeson_id, 42 | 'Fields': 'Genres,Overview', 43 | 'IncludeItemTypes': 'Episode', 44 | 'Recursive': 'true', 45 | 'SortBy': 'SortName', 46 | 'SortOrder': 'Ascending' 47 | } 48 | epoisode_response = session.get( 49 | f'{EMBY_SERVER}/emby/Items', headers=headers, params=params) 50 | episodes = epoisode_response.json()['Items'] 51 | for episode in episodes: 52 | episode_id = episode['Id'] 53 | episode_name = episode['Name'] 54 | params = {'Ids': episode_id} 55 | single_epoisode_response = session.get( 56 | f'{EMBY_SERVER}/emby/Users/{USER_ID}/Items/{episode_id}?Fields=ChannelMappingInfo&api_key={API_KEY}', headers=headers, params=params) 57 | episode = single_epoisode_response.json() 58 | if 'Genres' in episode: 59 | if len(episode['Genres']) != 0: 60 | genre = episode['Genres'] 61 | print( 62 | f' {series_name} {season_name} {episode_name} 清除genre {genre}') 63 | episode['Genres'] = [] 64 | episode['GenreItems'] = [] 65 | if not DRY_RUN: 66 | update_url = f'{EMBY_SERVER}/emby/Items/{episode_id}?api_key={API_KEY}&reqformat=json' 67 | response = session.post( 68 | update_url, json=episode, headers=headers) 69 | if response.status_code == 200 or response.status_code == 204: 70 | process_count += 1 71 | print( 72 | f' Successfully updated episode {episode_id} : {response.status_code} {response.content}') 73 | else: 74 | print( 75 | f' Failed to update episode {episode_id}: {response.status_code} {response.content}') 76 | 77 | 78 | def get_library_id(name): 79 | if not name: 80 | return 81 | res = session.get( 82 | f'{EMBY_SERVER}/emby/Library/VirtualFolders', headers=headers) 83 | lib_id = [i['ItemId'] for i in res.json() if i['Name'] == name] 84 | if not lib_id: 85 | raise KeyError(f'library: {name} not exists, check it') 86 | return lib_id[0] if lib_id else None 87 | 88 | 89 | def get_lib_items(parent_id): 90 | params = {'ParentId': parent_id, 91 | # 'HasTmdbId': True, 92 | 'fields': 'ProviderIds' 93 | } 94 | response = session.get(f'{EMBY_SERVER}/emby/Items', 95 | headers=headers, params=params) 96 | items = response.json()['Items'] 97 | items_folder = [item for item in items if item["Type"] == "Folder"] 98 | items = [item for item in items if item["Type"] != "Folder"] 99 | for folder in items_folder: 100 | items = items + get_lib_items(folder['Id']) 101 | 102 | return items 103 | 104 | 105 | def run_deleter(): 106 | libs = LIB_NAME.split(',') 107 | for lib_name in libs: 108 | parent_id = get_library_id(lib_name.strip()) 109 | series = get_lib_items(parent_id) 110 | print(f'**库 {lib_name} 中共有{len(series)} 个剧集,开始处理') 111 | for serie in series: 112 | serie_id = serie['Id'] 113 | remove_genre_for_episodes(serie_id) 114 | 115 | print(f'**更新成功{process_count}条') 116 | 117 | if __name__ == '__main__': 118 | run_deleter() 119 | 120 | -------------------------------------------------------------------------------- /genre_mapper/genre_mapper.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import logging 4 | 5 | config = { 6 | # 设置 Emby 服务器地址 7 | 'EMBY_SERVER' :'http://xxx:8096', 8 | # 设置 Emby 服务器APIKEY和userid 9 | 'API_KEY' : '', 10 | 'USER_ID' : '', 11 | # 库名, 多个时英文逗号分隔, 只支持剧集/电影库 12 | 'LIB_NAME' : '', 13 | # True 时为预览效果, False 实际写入 14 | 'DRY_RUN' : True, 15 | } 16 | 17 | 18 | # 需要替换的Genre 19 | genre_mapping = { 20 | 'Sci-Fi & Fantasy': { 21 | 'Name': '科幻', 'Id': 16630}, 22 | 'War & Politics': { 23 | 'Name': '战争', 'Id': 16718}, 24 | } 25 | # 需要移除的Genre 26 | genre_remove = [''] 27 | 28 | 29 | def emby_headers(): 30 | return { 31 | 'X-Emby-Token': config['API_KEY'], 32 | 'Content-Type': 'application/json', 33 | } 34 | 35 | session = requests.session() 36 | 37 | process_count = 0 38 | 39 | log = logging.getLogger('genre_mapper') 40 | log.setLevel(logging.DEBUG) 41 | formatter = logging.Formatter( 42 | '%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') 43 | fh = logging.FileHandler('logs.log', encoding='utf-8') 44 | fh.setLevel(logging.DEBUG) 45 | fh.setFormatter(formatter) 46 | ch = logging.StreamHandler() 47 | ch.setLevel(logging.DEBUG) 48 | ch.setFormatter(formatter) 49 | log.addHandler(ch) 50 | log.addHandler(fh) 51 | 52 | 53 | def remove_genre_for_episodes(parent_id): 54 | global process_count 55 | # 获取剧集列表 56 | params = {'Ids': parent_id} 57 | series_detail = session.get( 58 | f"{config['EMBY_SERVER']}/emby/Users/{config['USER_ID']}/Items/{parent_id}?Fields=ChannelMappingInfo&api_key={config['API_KEY']}", headers=emby_headers(), params=params) 59 | series = series_detail.json() 60 | genres = series['Genres'] 61 | genres_items = series['GenreItems'] 62 | need_replace = False 63 | for genre in genres: 64 | if genre in genre_mapping or genre in genre_remove: 65 | need_replace = True 66 | for genre_item in genres_items: 67 | if genre_item['Name'] in genre_mapping: 68 | need_replace = True 69 | 70 | if need_replace: 71 | log.info(f'{series["Name"]}:') 72 | genres_new = [genre_mapping[genre]['Name'] 73 | if genre in genre_mapping else genre for genre in genres] 74 | genres_new = list( 75 | filter(lambda genre: genre not in genre_remove, genres_new)) 76 | log.info(' '+str(series['Genres'])+"-->"+str(genres_new)) 77 | series['Genres'] = genres_new 78 | 79 | series['GenreItems'] = [genre_mapping[genre_item['Name']] if genre_item['Name'] 80 | in genre_mapping else genre_item for genre_item in genres_items] 81 | series['GenreItems'] = list( 82 | filter(lambda genre_item: genre_item['Name'] not in genre_remove, series['GenreItems'])) 83 | if not config['DRY_RUN']: 84 | update_url = f"{config['EMBY_SERVER']}/emby/Items/{parent_id}?api_key={config['API_KEY']}&reqformat=json" 85 | response = session.post( 86 | update_url, json=series, headers=emby_headers()) 87 | if response.status_code == 200 or response.status_code == 204: 88 | process_count += 1 89 | else: 90 | log.error(f' Failed to update series {parent_id}: {response.status_code} {response.content}') 91 | 92 | 93 | def get_library_id(name): 94 | if not name: 95 | return 96 | res = session.get( 97 | f"{config['EMBY_SERVER']}/emby/Library/VirtualFolders", headers=emby_headers()) 98 | lib_id = [i['ItemId'] for i in res.json() if i['Name'] == name] 99 | if not lib_id: 100 | raise KeyError(f'library: {name} not exists, check it') 101 | return lib_id[0] if lib_id else None 102 | 103 | 104 | def get_lib_items(parent_id): 105 | params = {'ParentId': parent_id, 106 | # 'HasTmdbId': True, 107 | 'fields': 'ProviderIds' 108 | } 109 | response = session.get(f"{config['EMBY_SERVER']}/emby/Items", 110 | headers=emby_headers(), params=params) 111 | items = response.json()['Items'] 112 | items_folder = [item for item in items if item["Type"] == "Folder"] 113 | items = [item for item in items if item["Type"] != "Folder"] 114 | for folder in items_folder: 115 | items = items + get_lib_items(folder['Id']) 116 | 117 | return items 118 | 119 | def run_mapper(): 120 | libs = config['LIB_NAME'].split(',') 121 | for lib_name in libs: 122 | parent_id = get_library_id(lib_name.strip()) 123 | series = get_lib_items(parent_id) 124 | log.info(f'**库 {lib_name} 中共有{len(series)} 个剧集,开始处理') 125 | for serie in series: 126 | serie_id = serie['Id'] 127 | remove_genre_for_episodes(serie_id) 128 | 129 | log.info(f'**更新成功{process_count}条') 130 | 131 | if __name__ == '__main__': 132 | run_mapper() 133 | 134 | -------------------------------------------------------------------------------- /theme_song_scraper/theme_song_scraper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import requests 4 | import json 5 | import shutil 6 | import time 7 | from yt_dlp import YoutubeDL 8 | 9 | SERIES_DIR = [ 10 | r"Z:\Anime", 11 | ] 12 | MOVIE_DIR = [ 13 | r"W:\Movie", 14 | ] 15 | # 是否下载电影的视频 16 | DOWNLOAD_MOVIE_BACKDROPS = True 17 | 18 | count = 0 19 | session = requests.Session() 20 | theme_file = "theme.mp3" 21 | theme_file_m4a = "theme.m4a" 22 | backdrop_dir = "backdrops" 23 | movie_theme_db = [] 24 | current_dir = os.path.abspath(os.path.dirname(__file__)) 25 | json_file = os.path.join(current_dir, "lizardbyte.json") 26 | 27 | 28 | def download_movie_theme_json(): 29 | if os.path.exists(json_file): 30 | pass 31 | else: 32 | download_file( 33 | "", 34 | json_file, 35 | ) 36 | 37 | 38 | def load_movie_theme_json(): 39 | download_movie_theme_json() 40 | global movie_theme_db 41 | try: 42 | f = open(json_file, "rb") 43 | content = f.read() 44 | movie_theme_db = json.loads(content) 45 | print(f"load lizardbyte.json success, {len(movie_theme_db)} items found") 46 | except Exception as ex: 47 | pass 48 | 49 | 50 | def get_tvdb_id(nfo_file, pattern=r'(.*?)'): 51 | with open(nfo_file, "r", encoding="utf-8", errors="ignore") as file: 52 | content = file.read() 53 | matches = re.findall(pattern, content) 54 | for match in matches: 55 | return match 56 | return None 57 | 58 | 59 | def get_json(url): 60 | response = session.get(url) 61 | if response.status_code == 200: 62 | resp_json = response.json() 63 | return resp_json 64 | else: 65 | return None 66 | 67 | 68 | def download_file(url, dest): 69 | global count 70 | dest_dir = os.path.dirname(dest) 71 | response = session.get(url) 72 | if response.status_code == 200: 73 | with open(dest, "wb") as file: 74 | file.write(response.content) 75 | count += 1 76 | print(f"{dest} download success") 77 | else: 78 | print(f"{dest_dir} no theme song found") 79 | pass 80 | 81 | 82 | def process_series(): 83 | for lib in SERIES_DIR: 84 | if not os.path.exists(lib): 85 | print(f"{lib} not found") 86 | continue 87 | items = os.listdir(lib) 88 | for item in items: 89 | item_dir = os.path.join(lib, item) 90 | item_files = os.listdir(item_dir) 91 | if theme_file in item_files: 92 | # print(f"{item_dir} 已经存在theme.mp3 跳过") 93 | pass 94 | else: 95 | tv_nfo = os.path.join(item_dir, "tvshow.nfo") 96 | if os.path.exists(tv_nfo): 97 | tvdb_id = get_tvdb_id(tv_nfo) 98 | if tvdb_id: 99 | url = f"http://tvthemes.plexapp.com/{tvdb_id}.mp3" 100 | dest = os.path.join(item_dir, theme_file) 101 | download_file(url, dest) 102 | else: 103 | # print(f"no tvdbid found with {item} ") 104 | pass 105 | 106 | 107 | def get_dirs_have_nfo(dir): 108 | res = [] 109 | childs = os.listdir(dir) 110 | for child in childs: 111 | child_abs_path = os.path.join(dir, child) 112 | if child.endswith(".nfo") and dir not in res: 113 | res.append(dir) 114 | if os.path.isdir(child_abs_path): 115 | res = res + get_dirs_have_nfo(child_abs_path) 116 | return res 117 | 118 | 119 | def download_audio(link,try_time = 0): 120 | if try_time >=3: 121 | return None 122 | try: 123 | with YoutubeDL( 124 | { 125 | "extract_audio": True, 126 | "format": "m4a/mp3", 127 | "quiet": True, 128 | "outtmpl": f"temp/theme.%(ext)s", 129 | } 130 | ) as video: 131 | video.download(link) 132 | return True 133 | except Exception as ex: 134 | time.sleep(try_time*5) 135 | download_audio(link,try_time+1) 136 | 137 | 138 | def download_video(link,try_time = 0): 139 | if try_time >=3: 140 | return None 141 | try: 142 | ydl_opts = { 143 | "format": "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best", 144 | "writethumbnail": False, 145 | "write_all_thumbnails": False, 146 | "outtmpl": f"temp/%(title)s.mp4", 147 | "verbose": False, 148 | "quiet": True, 149 | } 150 | with YoutubeDL(ydl_opts) as video: 151 | video.download(link) 152 | return True 153 | except Exception as ex: 154 | time.sleep(try_time*5) 155 | download_video(link,try_time+1) 156 | 157 | 158 | def video_file_path(path): 159 | childs = os.listdir(path) 160 | for child in childs: 161 | if child.endswith(".mp4"): 162 | return os.path.join(path, child) 163 | return None 164 | 165 | def audio_file_path(path): 166 | childs = os.listdir(path) 167 | for child in childs: 168 | if child.endswith(".m4a") or child.endswith('.mp3'): 169 | return os.path.join(path, child) 170 | return None 171 | 172 | 173 | def download_theme_for_movies(theme_url, dest_dir): 174 | global count 175 | youtube_url = theme_url 176 | childs = os.listdir(dest_dir) 177 | if theme_file not in childs and theme_file_m4a not in childs: 178 | print(f"start downloading theme audio for {dest_dir}") 179 | audio = download_audio(youtube_url) 180 | if not audio: 181 | return 182 | audio_file = audio_file_path("temp/") 183 | res = shutil.move(audio_file, dest_dir) 184 | count += 1 185 | print(f"download success to {res}") 186 | else: 187 | print(f"--{dest_dir}/theme.mp3 existed, skip") 188 | if DOWNLOAD_MOVIE_BACKDROPS and backdrop_dir not in childs: 189 | print(f"start downloading backdrops for {dest_dir}") 190 | video = download_video(youtube_url) 191 | if not video: 192 | return 193 | video_file = video_file_path("temp/") 194 | backdrop_path = os.path.join(dest_dir, backdrop_dir) 195 | os.makedirs(backdrop_path) 196 | res = shutil.move(video_file, os.path.join(dest_dir, backdrop_path)) 197 | count += 1 198 | print(f"download success to {res}") 199 | else: 200 | print(f"--{dest_dir}/backdrops existed, skip") 201 | 202 | 203 | def process_movies(): 204 | load_movie_theme_json() 205 | for lib in MOVIE_DIR: 206 | if not os.path.exists(lib): 207 | print(f"{lib} not found") 208 | continue 209 | 210 | nfo_dirs = get_dirs_have_nfo(lib) 211 | 212 | for nfo_dir in nfo_dirs: 213 | childs = os.listdir(nfo_dir) 214 | if (theme_file in childs or theme_file_m4a in childs) and (backdrop_dir in childs or not DOWNLOAD_MOVIE_BACKDROPS): 215 | print(f"--skip {nfo_dir}") 216 | else: 217 | nfo_file = next(child for child in childs if child.endswith(".nfo")) 218 | nfo_file = os.path.join(nfo_dir, nfo_file) 219 | tmdb_id = get_tvdb_id( 220 | nfo_file, r'(.*?)' 221 | ) 222 | imdb_id = get_tvdb_id( 223 | nfo_file, r'(.*?)' 224 | ) 225 | for db_item in movie_theme_db: 226 | if str(db_item["id"]) == tmdb_id: 227 | download_theme_for_movies(db_item['theme'], nfo_dir) 228 | break 229 | elif db_item["imdb_id"] == imdb_id: 230 | download_theme_for_movies(db_item['theme'], nfo_dir) 231 | break 232 | 233 | 234 | process_series() 235 | process_movies() 236 | print(f'download {count} theme songs for total') 237 | -------------------------------------------------------------------------------- /season_renamer/season_renamer.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import os 4 | import logging 5 | from dateutil import parser 6 | import datetime 7 | 8 | config = { 9 | # 设置 Emby 服务器地址 10 | 'EMBY_SERVER': 'http://xxx:8096', 11 | # 设置 Emby 服务器APIKEY和userid 12 | 'API_KEY': '', 13 | 'USER_ID': '', 14 | # 设置 TMDB_KEY 15 | 'TMDB_KEY': '', 16 | # 库名, 多个时英文逗号分隔, 只支持剧集/电影库 17 | 'LIB_NAME': '', 18 | # True 时为预览效果, False 实际写入 19 | 'DRY_RUN': True, 20 | } 21 | 22 | log = logging.getLogger('season_renamer') 23 | log.setLevel(logging.DEBUG) 24 | formatter = logging.Formatter( 25 | '%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') 26 | fh = logging.FileHandler('logs.log', encoding='utf-8') 27 | fh.setLevel(logging.DEBUG) 28 | fh.setFormatter(formatter) 29 | ch = logging.StreamHandler() 30 | ch.setLevel(logging.DEBUG) 31 | ch.setFormatter(formatter) 32 | log.addHandler(ch) 33 | log.addHandler(fh) 34 | 35 | 36 | def emby_headers(): 37 | return { 38 | 'X-Emby-Token': config['API_KEY'], 39 | 'Content-Type': 'application/json', 40 | } 41 | 42 | 43 | session = requests.session() 44 | 45 | process_count = 0 46 | 47 | 48 | class JsonDataBase: 49 | def __init__(self, name, prefix='', db_type='dict', workdir=None): 50 | self.file_name = f'{prefix}_{name}.json' if prefix else f'{name}.json' 51 | self.file_path = os.path.join( 52 | workdir, self.file_name) if workdir else self.file_name 53 | self.db_type = db_type 54 | self.data = self.load() 55 | 56 | def load(self, encoding='utf-8'): 57 | try: 58 | with open(self.file_path, encoding=encoding) as f: 59 | _json = json.load(f) 60 | except (FileNotFoundError, ValueError): 61 | # log.info(f'{self.file_name} not exist, return {self.db_type}') 62 | return dict(list=[], dict={})[self.db_type] 63 | else: 64 | return _json 65 | 66 | def dump(self, obj, encoding='utf-8'): 67 | with open(self.file_path, 'w', encoding=encoding) as f: 68 | json.dump(obj, f, indent=2, ensure_ascii=False) 69 | 70 | def save(self): 71 | self.dump(self.data) 72 | 73 | 74 | class TmdbDataBase(JsonDataBase): 75 | def __getitem__(self, tmdb_id): 76 | data = self.data.get(tmdb_id) 77 | if not data: 78 | return 79 | air_date = datetime.date.today() 80 | try: 81 | air_date = parser.parse(data['premiere_date']).date() 82 | except Exception as ex: 83 | pass 84 | today = datetime.date.today() 85 | if air_date + datetime.timedelta(days=30) > today: 86 | expire_day = 3 87 | elif air_date + datetime.timedelta(days=90) > today: 88 | expire_day = 15 89 | elif air_date + datetime.timedelta(days=365) > today: 90 | expire_day = 30 91 | else: 92 | expire_day = 30 93 | update_date = datetime.date.fromisoformat(data['update_date']) 94 | if update_date + datetime.timedelta(days=expire_day) < today: 95 | return 96 | return data 97 | 98 | def __setitem__(self, key, value): 99 | self.data[key] = value 100 | self.save() 101 | 102 | def clean_not_trust_data(self, expire_days=7, min_trust=0.5): 103 | expire_days = datetime.timedelta(days=expire_days) 104 | today = datetime.date.today() 105 | self.data = {_id: info for _id, info in self.data.items() 106 | if info['trust'] >= min_trust or 107 | datetime.date.fromisoformat(info['update_date']) + expire_days > today} 108 | self.save() 109 | 110 | def save_seasons(self, tmdb_id, premiere_date, name, alt_names, seasons=None): 111 | self.data[tmdb_id] = { 112 | 'premiere_date': premiere_date, 113 | 'name': name, 114 | 'alt_names': alt_names, 115 | 'seasons': seasons, 116 | 'update_date': str(datetime.date.today()), 117 | } 118 | if process_count % 20 == 0: 119 | self.save() 120 | 121 | 122 | def get_or_default(_dict, key, default=None): 123 | return _dict[key] if key in _dict else default 124 | 125 | 126 | tmdb_db = TmdbDataBase('tmdb') 127 | 128 | 129 | def get_season_info_from_tmdb(tmdb_id, is_movie, serie_name): 130 | cache_key = ('mv' if is_movie else 'tv') + f'{tmdb_id}' 131 | cache_data = tmdb_db[cache_key] 132 | if cache_data: 133 | alt_names = cache_data['seasons'] 134 | return alt_names, True 135 | url = f"https://api.themoviedb.org/3/tv/{tmdb_id}?language=zh-CN&append_to_response=alternative_titles" 136 | try: 137 | response = session.get(url, headers={ 138 | "accept": "application/json", 139 | "Authorization": f"Bearer {config['TMDB_KEY']}" 140 | }) 141 | resp_json = response.json() 142 | except Exception as ex: 143 | log.exception(ex) 144 | return None, None 145 | if 'seasons' in resp_json: 146 | titles = resp_json["alternative_titles"] 147 | release_date = get_or_default( 148 | resp_json, 'last_air_date', default=get_or_default(resp_json, 'first_air_date')) 149 | alt_names = get_or_default( 150 | titles, "results", None) 151 | tmdb_db.save_seasons(cache_key, premiere_date=release_date, 152 | name=serie_name, alt_names=alt_names, seasons=resp_json['seasons']) 153 | return resp_json['seasons'], False 154 | else: 155 | 156 | return None, None 157 | 158 | 159 | def rename_seasons(parent_id, tmdb_id, series_name, is_movie): 160 | global process_count 161 | # 获取剧集列表 162 | params = {'ParentId': parent_id} 163 | response = session.get(f"{config['EMBY_SERVER']}/emby/Items", 164 | headers=emby_headers(), params=params) 165 | 166 | tmdb_seasons, is_cache = get_season_info_from_tmdb( 167 | tmdb_id, is_movie, series_name) 168 | from_cache = ' fromcache ' if is_cache else '' 169 | if not tmdb_seasons: 170 | log.error(f' no season found in tmdb:{tmdb_id} {series_name}') 171 | return 172 | seasons = response.json()['Items'] 173 | for season in seasons: 174 | seaeson_id = season['Id'] 175 | season_name = season['Name'] 176 | series_name = season['SeriesName'] 177 | if 'IndexNumber' not in season: 178 | log.info(f' {series_name} {season_name} 没有编号,跳过') 179 | continue 180 | season_index = season['IndexNumber'] 181 | tmdb_season = tmdb_seasons 182 | tmdb_season = next( 183 | (season for season in tmdb_seasons if season['season_number'] == season_index), None) 184 | if tmdb_season: 185 | tmdb_season_name = tmdb_season['name'] 186 | single_season_response = session.get( 187 | f"{config['EMBY_SERVER']}/emby/Users/{config['USER_ID']}/Items/{seaeson_id}?Fields=ChannelMappingInfo&api_key={config['API_KEY']}", headers=emby_headers(), params=params) 188 | single_season = single_season_response.json() 189 | if 'Name' in single_season: 190 | if season_name == tmdb_season_name: 191 | if get_or_default(config, 'IS_DOCKER') != True: 192 | log.info( 193 | f' {series_name} 第{season_index}季 {from_cache} [{season_name}] 季名一致 跳过更新') 194 | continue 195 | else: 196 | log.info( 197 | f' {series_name} 第{season_index}季 {from_cache} 将从 [{season_name}] 更名为 [{tmdb_season_name}]') 198 | single_season['Name'] = tmdb_season_name 199 | if 'LockedFields' not in single_season: 200 | single_season['LockedFields'] = [] 201 | if 'Name' not in single_season['LockedFields']: 202 | single_season['LockedFields'].append('Name') 203 | if not config['DRY_RUN']: 204 | update_url = f"{config['EMBY_SERVER']}/emby/Items/{seaeson_id}?api_key={config['API_KEY']}&reqformat=json" 205 | response = session.post( 206 | update_url, json=single_season, headers=emby_headers()) 207 | if response.status_code == 200 or response.status_code == 204: 208 | process_count += 1 209 | # log.info(f' Successfully updated {series_name} {season_name} : {response.status_code} {response.content}') 210 | else: 211 | log.info( 212 | f' Failed to update {series_name} {season_name}: {response.status_code} {response.content}') 213 | 214 | 215 | def get_library_id(name): 216 | if not name: 217 | return 218 | res = session.get( 219 | f"{config['EMBY_SERVER']}/emby/Library/VirtualFolders", headers=emby_headers()) 220 | lib_id = [i['ItemId'] for i in res.json() if i['Name'] == name] 221 | if not lib_id: 222 | raise KeyError(f'library: {name} not exists, check it') 223 | return lib_id[0] if lib_id else None 224 | 225 | 226 | def get_lib_items(parent_id): 227 | params = {'ParentId': parent_id, 228 | # 'HasTmdbId': True, 229 | 'fields': 'ProviderIds' 230 | } 231 | response = session.get(f"{config['EMBY_SERVER']}/emby/Items", 232 | headers=emby_headers(), params=params) 233 | items = response.json()['Items'] 234 | items_folder = [item for item in items if item["Type"] == "Folder"] 235 | items = [item for item in items if item["Type"] != "Folder"] 236 | for folder in items_folder: 237 | items = items + get_lib_items(folder['Id']) 238 | 239 | return items 240 | 241 | 242 | def run_renamer(): 243 | libs = config['LIB_NAME'].split(',') 244 | for lib_name in libs: 245 | parent_id = get_library_id(lib_name.strip()) 246 | series = get_lib_items(parent_id) 247 | log.info(f'**库 {lib_name} 中共有{len(series)} 个剧集,开始处理') 248 | 249 | for serie in series: 250 | serie_id = serie['Id'] 251 | serie_name = serie['Name'] 252 | is_movie = serie['Type'] == 'Movie' 253 | tmdb_id = '' 254 | if is_movie: 255 | continue 256 | if 'ProviderIds' in serie and 'Tmdb' in serie['ProviderIds']: 257 | tmdb_id = serie['ProviderIds']['Tmdb'] 258 | rename_seasons(serie_id, tmdb_id, serie_name, is_movie) 259 | else: 260 | log.error(f'error:{serie_name} has no tmdb id, skip') 261 | 262 | tmdb_db.save() 263 | log.info(f'**更新成功{process_count}条') 264 | 265 | 266 | if __name__ == '__main__': 267 | run_renamer() 268 | -------------------------------------------------------------------------------- /country_scraper/country_scraper.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import os 4 | import logging 5 | from dateutil import parser 6 | import datetime 7 | 8 | config = { 9 | # 设置 Emby 服务器地址 10 | 'EMBY_SERVER': 'http://xxx:8096', 11 | # 设置 Emby 服务器APIKEY和userid 12 | 'API_KEY': '', 13 | 'USER_ID': '', 14 | # 设置 TMDB_KEY(API 读访问令牌) 15 | 'TMDB_KEY': '', 16 | # 库名, 多个时英文逗号分隔, 只支持剧集/电影库 17 | 'LIB_NAME': '', 18 | # True 时为预览效果, False 实际写入 19 | 'DRY_RUN': True, 20 | } 21 | 22 | country_dict = { 23 | 'KR': '韩国', 24 | 'CN': '中国', 25 | 'HK': '香港', 26 | 'TW': '台湾', 27 | 'JP': '日本', 28 | 'US': '美国', 29 | 'GB': '英国', 30 | 'FR': '法国', 31 | 'DE': '德国', 32 | 'IN': '印度', 33 | 'RU': '俄罗斯', 34 | 'CA': '加拿大', 35 | 36 | } 37 | DEFAULT_COUNTRY = '其他国家' 38 | DEFAULT_LANGUAGE = '其他语种' 39 | language_dict = { 40 | 'cn': '粤语', 41 | 'zh': '国语', 42 | 'ja': '日语', 43 | 'en': '英语', 44 | 'ko': '韩语', 45 | 'fr': '法语', 46 | 'de': '德语', 47 | 'ru': '俄语', 48 | 'es': '西班牙语', 49 | } 50 | 51 | 52 | log = logging.getLogger('country_scraper') 53 | log.setLevel(logging.DEBUG) 54 | formatter = logging.Formatter( 55 | '%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') 56 | fh = logging.FileHandler('logs.log', encoding='utf-8') 57 | fh.setLevel(logging.DEBUG) 58 | fh.setFormatter(formatter) 59 | ch = logging.StreamHandler() 60 | ch.setLevel(logging.DEBUG) 61 | ch.setFormatter(formatter) 62 | log.addHandler(ch) 63 | log.addHandler(fh) 64 | 65 | 66 | def emby_headers(): 67 | return { 68 | 'X-Emby-Token': config['API_KEY'], 69 | 'Content-Type': 'application/json', 70 | } 71 | 72 | 73 | session = requests.session() 74 | 75 | process_count = 0 76 | 77 | 78 | class JsonDataBase: 79 | def __init__(self, name, prefix='', db_type='dict', workdir=None): 80 | self.file_name = f'{prefix}_{name}.json' if prefix else f'{name}.json' 81 | self.file_path = os.path.join( 82 | workdir, self.file_name) if workdir else self.file_name 83 | self.db_type = db_type 84 | self.data = self.load() 85 | 86 | def load(self, encoding='utf-8'): 87 | try: 88 | with open(self.file_path, encoding=encoding) as f: 89 | _json = json.load(f) 90 | except (FileNotFoundError, ValueError): 91 | # log.info(f'{self.file_name} not exist, return {self.db_type}') 92 | return dict(list=[], dict={})[self.db_type] 93 | else: 94 | return _json 95 | 96 | def dump(self, obj, encoding='utf-8'): 97 | with open(self.file_path, 'w', encoding=encoding) as f: 98 | json.dump(obj, f, indent=2, ensure_ascii=False) 99 | 100 | def save(self): 101 | self.dump(self.data) 102 | 103 | 104 | class TmdbDataBase(JsonDataBase): 105 | def __getitem__(self, tmdb_id): 106 | data = self.data.get(tmdb_id) 107 | if not data: 108 | return 109 | air_date = datetime.date.today() 110 | try: 111 | air_date = parser.parse(data['premiere_date']).date() 112 | except Exception as ex: 113 | pass 114 | today = datetime.date.today() 115 | if air_date + datetime.timedelta(days=30) > today: 116 | expire_day = 3 117 | elif air_date + datetime.timedelta(days=90) > today: 118 | expire_day = 15 119 | elif air_date + datetime.timedelta(days=365) > today: 120 | expire_day = 30 121 | else: 122 | expire_day = 365 123 | update_date = datetime.date.fromisoformat(data['update_date']) 124 | if update_date + datetime.timedelta(days=expire_day) < today: 125 | return 126 | return data 127 | 128 | def __setitem__(self, key, value): 129 | self.data[key] = value 130 | self.save() 131 | 132 | def clean_not_trust_data(self, expire_days=7, min_trust=0.5): 133 | expire_days = datetime.timedelta(days=expire_days) 134 | today = datetime.date.today() 135 | self.data = {_id: info for _id, info in self.data.items() 136 | if info['trust'] >= min_trust or 137 | datetime.date.fromisoformat(info['update_date']) + expire_days > today} 138 | self.save() 139 | 140 | def save_country(self, tmdb_id, premiere_date, name, production_countries, spoken_languages): 141 | self.data[tmdb_id] = { 142 | 'premiere_date': premiere_date, 143 | 'name': name, 144 | 'production_countries': production_countries, 145 | 'spoken_languages': spoken_languages, 146 | 'update_date': str(datetime.date.today()), 147 | } 148 | if process_count % 20 == 0: 149 | self.save() 150 | 151 | 152 | def get_or_default(_dict, key, default=None): 153 | return _dict[key] if key in _dict else default 154 | 155 | 156 | tmdb_db = TmdbDataBase('country') 157 | 158 | 159 | def get_country_info_from_tmdb(tmdb_id, serie_name, is_movie=False): 160 | cache_key = ('mv' if is_movie else 'tv') + f'{tmdb_id}' 161 | cache_data = tmdb_db[cache_key] 162 | if cache_data and 'production_countries' in cache_data: 163 | production_countries = cache_data["production_countries"] 164 | spoken_languages = cache_data["spoken_languages"] 165 | return production_countries, spoken_languages, True 166 | 167 | try: 168 | url = f"https://api.themoviedb.org/3/{'movie' if is_movie else 'tv'}/{tmdb_id}?language=zh-CN" 169 | response = session.get(url, headers={ 170 | "accept": "application/json", 171 | "Authorization": f"Bearer {config['TMDB_KEY']}" 172 | }) 173 | resp_json = response.json() 174 | except Exception as ex: 175 | log.exception(ex) 176 | return None, None, None 177 | # print(resp_json) 178 | if "production_countries" in resp_json or "spoken_languages" in resp_json: 179 | production_countries = resp_json["production_countries"] 180 | spoken_languages = resp_json["spoken_languages"] 181 | release_date = get_or_default(resp_json, 'release_date') if is_movie else get_or_default( 182 | resp_json, 'last_air_date', default=get_or_default(resp_json, 'first_air_date')) 183 | tmdb_db.save_country( 184 | cache_key, premiere_date=release_date, name=serie_name, production_countries=production_countries, spoken_languages=spoken_languages) 185 | return production_countries, spoken_languages, True 186 | else: 187 | log.error(f' no result found in tmdb:{serie_name} {resp_json}') 188 | return None, None, None 189 | 190 | 191 | def add_country(parent_id, tmdb_id, serie_name, is_movie): 192 | global process_count 193 | production_countries, spoken_languages, is_cache = get_country_info_from_tmdb( 194 | tmdb_id, serie_name, is_movie=is_movie) 195 | from_cache = ' fromcache ' if is_cache else '' 196 | if not production_countries and not spoken_languages: 197 | if get_or_default(config, 'IS_DOCKER') != True: 198 | log.info(f' {serie_name} {from_cache} 没有设置国家 跳过') 199 | return 200 | 201 | item_response = session.get( 202 | f"{config['EMBY_SERVER']}/emby/Users/{config['USER_ID']}/Items/{parent_id}?Fields=ChannelMappingInfo&api_key={config['API_KEY']}", headers=emby_headers()) 203 | item = item_response.json() 204 | 205 | series_name = item['Name'] 206 | old_tags = item['TagItems'] 207 | old_tags = [tag['Name']for tag in old_tags] 208 | 209 | new_tags = old_tags[:] 210 | 211 | tmdb_countries = [] 212 | for country in production_countries: 213 | tag = get_or_default( 214 | country_dict, country['iso_3166_1'], DEFAULT_COUNTRY) 215 | if tag not in tmdb_countries: 216 | tmdb_countries.append(tag) 217 | 218 | for country in tmdb_countries: 219 | if country not in new_tags: 220 | if country != DEFAULT_COUNTRY or len(tmdb_countries) <= 2: 221 | new_tags.append(country) 222 | 223 | tmdb_languages = [] 224 | for language in spoken_languages: 225 | tag = get_or_default( 226 | language_dict, language['iso_639_1'], DEFAULT_LANGUAGE) 227 | if tag not in new_tags: 228 | tmdb_languages.append(tag) 229 | 230 | for language in tmdb_languages: 231 | if language not in new_tags: 232 | if language != DEFAULT_LANGUAGE or len(tmdb_languages) <= 2: 233 | new_tags.append(language) 234 | 235 | if new_tags == old_tags: 236 | if get_or_default(config, 'IS_DOCKER') != True: 237 | log.info(f' {serie_name} {from_cache} 标签没有变化 跳过') 238 | return 239 | else: 240 | log.info(f' {serie_name} {from_cache} 设置标签为 {new_tags}') 241 | 242 | item['Tags'] = new_tags 243 | if 'TagItems' not in item: 244 | item['TagItems'] = [] 245 | 246 | for tag in new_tags: 247 | if tag not in old_tags: 248 | item['TagItems'].append({'Name': tag}) 249 | 250 | if 'LockedFields' not in item: 251 | item['LockedFields'] = [] 252 | if 'Tags' not in item['LockedFields']: 253 | item['LockedFields'].append('Tags') 254 | 255 | if not config['DRY_RUN']: 256 | update_url = f"{config['EMBY_SERVER']}/emby/Items/{parent_id}?api_key={config['API_KEY']}&reqformat=json" 257 | response = session.post(update_url, json=item, headers=emby_headers()) 258 | if response.status_code == 200 or response.status_code == 204: 259 | process_count += 1 260 | # log.info(f' Successfully updated {series_name} {season_name} : {response.status_code} {response.content}') 261 | else: 262 | log.info( 263 | f' Failed to update {series_name} : {response.status_code} {response.content}') 264 | 265 | 266 | def get_library_id(name): 267 | if not name: 268 | return 269 | res = session.get( 270 | f"{config['EMBY_SERVER']}/emby/Library/VirtualFolders", headers=emby_headers()) 271 | lib_id = [i['ItemId'] for i in res.json() if i['Name'] == name] 272 | if not lib_id: 273 | raise KeyError(f'library: {name} not exists, check it') 274 | return lib_id[0] if lib_id else None 275 | 276 | 277 | def get_lib_items(parent_id): 278 | params = {'ParentId': parent_id, 279 | # 'HasTmdbId': True, 280 | 'fields': 'ProviderIds' 281 | } 282 | response = session.get(f"{config['EMBY_SERVER']}/emby/Items", 283 | headers=emby_headers(), params=params) 284 | items = response.json()['Items'] 285 | items_folder = [item for item in items if item["Type"] == "Folder"] 286 | items = [item for item in items if item["Type"] != "Folder"] 287 | for folder in items_folder: 288 | items = items + get_lib_items(folder['Id']) 289 | 290 | return items 291 | 292 | 293 | def run_scraper(): 294 | libs = config['LIB_NAME'].split(',') 295 | for lib_name in libs: 296 | parent_id = get_library_id(lib_name.strip()) 297 | items = get_lib_items(parent_id) 298 | 299 | log.info(f'**库 {lib_name} 中共有{len(items)} 个Item, 开始处理') 300 | 301 | for item in items: 302 | item_id = item['Id'] 303 | item_name = item['Name'] 304 | is_movie = item['Type'] == 'Movie' 305 | if 'ProviderIds' in item and 'Tmdb' in item['ProviderIds']: 306 | tmdb_id = item['ProviderIds']['Tmdb'] 307 | add_country(item_id, tmdb_id, item_name, is_movie=is_movie) 308 | else: 309 | log.info(f'error:{item_name} has no tmdb id, skip') 310 | 311 | tmdb_db.save() 312 | log.info(f'**更新成功{process_count}条') 313 | 314 | 315 | if __name__ == '__main__': 316 | run_scraper() 317 | -------------------------------------------------------------------------------- /alternative_renamer/alternative_renamer.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import os 4 | import logging 5 | from dateutil import parser 6 | import datetime 7 | from opencc import OpenCC 8 | 9 | cc = OpenCC('t2s') 10 | 11 | config = { 12 | # 设置 Emby 服务器地址 13 | 'EMBY_SERVER' :'http://xxx:8096', 14 | # 设置 Emby 服务器APIKEY和userid 15 | 'API_KEY' : '', 16 | 'USER_ID' : '', 17 | # 设置 TMDB_KEY 18 | 'TMDB_KEY' : '', 19 | # 库名, 多个时英文逗号分隔, 只支持剧集/电影库 20 | 'LIB_NAME' : '', 21 | # True 时为预览效果, False 实际写入 22 | 'DRY_RUN' : True, 23 | 'ADD_HANT_TITLE' : True, 24 | } 25 | 26 | if not os.path.exists('logs'): 27 | os.makedirs('logs') 28 | 29 | log = logging.getLogger('alt_renamer') 30 | log.setLevel(logging.DEBUG) 31 | formatter = logging.Formatter( 32 | '%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') 33 | fh = logging.FileHandler('logs.log', encoding='utf-8') 34 | fh.setLevel(logging.DEBUG) 35 | fh.setFormatter(formatter) 36 | ch = logging.StreamHandler() 37 | ch.setLevel(logging.DEBUG) 38 | ch.setFormatter(formatter) 39 | log.addHandler(ch) 40 | log.addHandler(fh) 41 | 42 | def emby_headers(): 43 | return { 44 | 'X-Emby-Token': config['API_KEY'], 45 | 'Content-Type': 'application/json', 46 | } 47 | 48 | session = requests.session() 49 | 50 | process_count = 0 51 | 52 | 53 | class JsonDataBase: 54 | def __init__(self, name, prefix='', db_type='dict', workdir=None): 55 | self.file_name = f'{prefix}_{name}.json' if prefix else f'{name}.json' 56 | self.file_path = os.path.join( 57 | workdir, self.file_name) if workdir else self.file_name 58 | self.db_type = db_type 59 | self.data = self.load() 60 | 61 | def load(self, encoding='utf-8'): 62 | try: 63 | with open(self.file_path, encoding=encoding) as f: 64 | _json = json.load(f) 65 | except (FileNotFoundError, ValueError): 66 | # log.info(f'{self.file_name} not exist, return {self.db_type}') 67 | return dict(list=[], dict={})[self.db_type] 68 | else: 69 | return _json 70 | 71 | def dump(self, obj, encoding='utf-8'): 72 | with open(self.file_path, 'w', encoding=encoding) as f: 73 | json.dump(obj, f, indent=2, ensure_ascii=False) 74 | 75 | def save(self): 76 | self.dump(self.data) 77 | 78 | 79 | class TmdbDataBase(JsonDataBase): 80 | def __getitem__(self, tmdb_id): 81 | data = self.data.get(tmdb_id) 82 | if not data: 83 | return 84 | air_date = datetime.date.today() 85 | try: 86 | air_date = parser.parse(data['premiere_date']).date() 87 | except Exception as ex: 88 | pass 89 | today = datetime.date.today() 90 | if air_date + datetime.timedelta(days=30) > today: 91 | expire_day = 3 92 | elif air_date + datetime.timedelta(days=90) > today: 93 | expire_day = 15 94 | elif air_date + datetime.timedelta(days=365) > today: 95 | expire_day = 30 96 | else: 97 | expire_day = 365 98 | update_date = datetime.date.fromisoformat(data['update_date']) 99 | if update_date + datetime.timedelta(days=expire_day) < today: 100 | return 101 | return data 102 | 103 | def __setitem__(self, key, value): 104 | self.data[key] = value 105 | self.save() 106 | 107 | def clean_not_trust_data(self, expire_days=7, min_trust=0.5): 108 | expire_days = datetime.timedelta(days=expire_days) 109 | today = datetime.date.today() 110 | self.data = {_id: info for _id, info in self.data.items() 111 | if info['trust'] >= min_trust or 112 | datetime.date.fromisoformat(info['update_date']) + expire_days > today} 113 | self.save() 114 | 115 | def save_alt_name(self, tmdb_id, premiere_date, name, alt_names, seasons=None,hant_trans = None): 116 | self.data[tmdb_id] = { 117 | 'premiere_date': premiere_date, 118 | 'name': name, 119 | 'alt_names': alt_names, 120 | 'seasons': seasons, 121 | 'hant_trans': hant_trans, 122 | 'update_date': str(datetime.date.today()), 123 | } 124 | self.save() 125 | 126 | 127 | def get_or_default(_dict, key, default=None): 128 | return _dict[key] if key in _dict else default 129 | 130 | 131 | tmdb_db = TmdbDataBase('tmdb_alt_name') 132 | 133 | 134 | def get_alt_name_info_from_tmdb(tmdb_id, serie_name, is_movie=False): 135 | cache_key = ('mv' if is_movie else 'tv') + f'{tmdb_id}' 136 | cache_data = tmdb_db[cache_key] 137 | if cache_data and 'alt_names' in cache_data: 138 | alt_names = cache_data['alt_names'] 139 | hant_trans = cache_data['hant_trans'] 140 | return alt_names,hant_trans, True 141 | 142 | url = f"https://api.themoviedb.org/3/{'movie' if is_movie else 'tv'}/{tmdb_id}?append_to_response=alternative_titles,translations&language=zh-CN" 143 | try: 144 | response = session.get(url, headers={ 145 | "accept": "application/json", 146 | "Authorization": f"Bearer {config['TMDB_KEY']}" 147 | }) 148 | resp_json = response.json() 149 | except Exception as ex: 150 | log.exception(ex) 151 | return None,None,None 152 | if "alternative_titles" in resp_json: 153 | titles = resp_json["alternative_titles"] 154 | release_date = get_or_default(resp_json, 'release_date') if is_movie else get_or_default( 155 | resp_json, 'last_air_date', default=get_or_default(resp_json, 'first_air_date')) 156 | alt_names = get_or_default( 157 | titles, "titles" if is_movie else "results", None) 158 | translations = get_or_default(resp_json,"translations") 159 | translations = get_or_default(translations,"translations") 160 | hant_trans = [cc.convert(tran['data']['title' if is_movie else 'name']) 161 | for tran in translations if (tran["iso_3166_1"] == "HK" or tran["iso_3166_1"] == "TW") and len(tran['data']['title' if is_movie else 'name'].strip())!=0 ] 162 | # if not alt_names: 163 | # log.error(f' alt names missing in tmdb:{serie_name} {resp_json}') 164 | tmdb_db.save_alt_name( 165 | cache_key, premiere_date=release_date, name=serie_name, alt_names=alt_names, seasons=get_or_default(resp_json, 'seasons'),hant_trans=hant_trans) 166 | return alt_names,hant_trans, False 167 | else: 168 | log.error(f' no result found in tmdb:{serie_name} {resp_json}') 169 | return None,None,None 170 | 171 | 172 | arr_invalid_char = ['ā', 'á', 'ǎ', 'à', 173 | 'ē', 'é', 'ě', 'è', 174 | 'ī', 'í', 'ǐ', 'ì', 175 | 'ō', 'ó', 'ǒ', 'ò', 176 | 'ū', 'ú', 'ǔ', 'ù ', 177 | 'ǖ', 'ǘ', 'ǚ', 'ǜ', 178 | 'デ', 'ô', 'â', 'Ś', 'ü', 'É'] 179 | 180 | 181 | def invalid_char_in_str(name): 182 | exist = False 183 | for invalid_char in arr_invalid_char: 184 | if invalid_char in name: 185 | exist = True 186 | break 187 | return exist 188 | 189 | 190 | def add_alt_names(parent_id, tmdb_id, serie_name, is_movie): 191 | global process_count 192 | tmdb_alt_name, hant_trans, is_cache = get_alt_name_info_from_tmdb( 193 | tmdb_id, serie_name, is_movie=is_movie) 194 | from_cache = ' fromcache ' if is_cache else '' 195 | if not tmdb_alt_name and not hant_trans == 0: 196 | return 197 | 198 | if not tmdb_alt_name: 199 | tmdb_alt_name = [] 200 | else: 201 | tmdb_alt_name = [x['title'] 202 | for x in tmdb_alt_name if x["iso_3166_1"] == "CN"] 203 | 204 | if get_or_default(config,'ADD_HANT_TITLE') == True and hant_trans: 205 | tmdb_alt_name = tmdb_alt_name + hant_trans 206 | 207 | if len(tmdb_alt_name) == 0: 208 | if get_or_default(config,'IS_DOCKER') != True: 209 | log.info(f' {serie_name} {from_cache} 没有别名 跳过') 210 | return 211 | 212 | name_spliter = ' / ' 213 | item_response = session.get( 214 | f"{config['EMBY_SERVER']}/emby/Users/{config['USER_ID']}/Items/{parent_id}?Fields=ChannelMappingInfo&api_key={config['API_KEY']}", headers=emby_headers()) 215 | item = item_response.json() 216 | 217 | series_name = item['Name'] 218 | 219 | if 'SortName' in item: 220 | old_names = item['SortName'].split(name_spliter) 221 | res = [] 222 | for old_name in old_names: 223 | if old_name not in res: 224 | res.append(old_name) 225 | for new_name in tmdb_alt_name: 226 | if new_name not in res: 227 | if not invalid_char_in_str(new_name): 228 | res.append(new_name) 229 | 230 | sort_name_all = name_spliter.join(res) 231 | if old_names == res: 232 | if get_or_default(config,'IS_DOCKER') != True: 233 | log.info(f' {series_name} {from_cache} 别名没有增删 跳过') 234 | return 235 | else: 236 | log.info(f' {series_name} {from_cache} 增加别名 [{sort_name_all}]') 237 | item['SortName'] = sort_name_all 238 | item['ForcedSortName'] = sort_name_all 239 | # item['SortName'] = item['Name'] 240 | # item['ForcedSortName'] = item['Name'] 241 | if 'LockedFields' not in item: 242 | item['LockedFields'] = [] 243 | if 'SortName' not in item['LockedFields']: 244 | item['LockedFields'].append('SortName') 245 | 246 | if not config['DRY_RUN']: 247 | update_url = f"{config['EMBY_SERVER']}/emby/Items/{parent_id}?api_key={config['API_KEY']}&reqformat=json" 248 | response = session.post(update_url, json=item, headers=emby_headers()) 249 | if response.status_code == 200 or response.status_code == 204: 250 | process_count += 1 251 | # log.info(f' Successfully updated {series_name} {season_name} : {response.status_code} {response.content}') 252 | else: 253 | log.info( 254 | f' Failed to update {series_name} : {response.status_code} {response.content}') 255 | 256 | 257 | def get_library_id(name): 258 | if not name: 259 | return 260 | res = session.get( 261 | f"{config['EMBY_SERVER']}/emby/Library/VirtualFolders", headers=emby_headers()) 262 | lib_id = [i['ItemId'] for i in res.json() if i['Name'] == name] 263 | if not lib_id: 264 | raise KeyError(f'library: {name} not exists, check it') 265 | return lib_id[0] if lib_id else None 266 | 267 | 268 | def get_lib_items(parent_id): 269 | params = {'ParentId': parent_id, 270 | # 'HasTmdbId': True, 271 | 'fields': 'ProviderIds' 272 | } 273 | response = session.get(f"{config['EMBY_SERVER']}/emby/Items", 274 | headers=emby_headers(), params=params) 275 | items = response.json()['Items'] 276 | items_folder = [item for item in items if item["Type"] == "Folder"] 277 | items = [item for item in items if item["Type"] != "Folder"] 278 | for folder in items_folder: 279 | items = items + get_lib_items(folder['Id']) 280 | 281 | return items 282 | 283 | 284 | def run_renameer(): 285 | libs = config['LIB_NAME'].split(',') 286 | for lib_name in libs: 287 | parent_id = get_library_id(lib_name.strip()) 288 | items = get_lib_items(parent_id) 289 | 290 | log.info(f'**库 {lib_name} 中共有{len(items)} 个Item, 开始处理') 291 | 292 | for item in items: 293 | item_id = item['Id'] 294 | item_name = item['Name'] 295 | is_movie = item['Type'] == 'Movie' 296 | if 'ProviderIds' in item and 'Tmdb' in item['ProviderIds']: 297 | tmdb_id = item['ProviderIds']['Tmdb'] 298 | add_alt_names(item_id, tmdb_id, item_name, is_movie=is_movie) 299 | else: 300 | log.info(f'error:{item_name} has no tmdb id, skip') 301 | 302 | log.info(f'**更新成功{process_count}条') 303 | 304 | if __name__ == '__main__': 305 | run_renameer() 306 | --------------------------------------------------------------------------------