├── 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 | 
8 | 
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 | 
9 | 
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 | 
7 | 
8 |
9 |
10 | 标签封面:
11 | 
12 | 
13 | 
14 | 
15 | 
16 | 
17 | 
18 | 
19 | 
20 | 
21 | 
22 | 
23 | 
24 | 
25 | 
26 | 
27 | 
28 | 
29 | 
30 | 
31 | 
32 | 
33 | 
34 | 
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 |
--------------------------------------------------------------------------------