├── requirements.txt ├── docs ├── emby_1.png └── console_1.png ├── config.conf ├── README.md ├── .gitignore └── EMBY_HotMovie_Importer.py /requirements.txt: -------------------------------------------------------------------------------- 1 | configparser 2 | feedparser 3 | requests -------------------------------------------------------------------------------- /docs/emby_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuanqb/EMBY_HotMovie_Importer/HEAD/docs/emby_1.png -------------------------------------------------------------------------------- /docs/console_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuanqb/EMBY_HotMovie_Importer/HEAD/docs/console_1.png -------------------------------------------------------------------------------- /config.conf: -------------------------------------------------------------------------------- 1 | [Server] 2 | # 这里填入你Emby服务器地址 3 | emby_server = http://xxx.xx.xx.x:8096 4 | # 这里填入你Emby API密钥 5 | emby_api_key = xxxxxxx 6 | rsshub_server = http://xx.xx.x.x:1200 7 | 8 | [Collection] 9 | #各种榜单 10 | rss_ids=movie_weekly_best,tv_global_best_weekly,tv_chinese_best_weekly,show_chinese_best_weekly,show_global_best_weekly,movie_hot_gaia,tv_hot,show_hot,movie_top250,subject_real_time_hotest 11 | 12 | # 指定是否需要使用代理,如果是WIN本地Clash,则开启 13 | [Proxy] 14 | use_proxy = False 15 | # use_proxy = True 16 | http_proxy = http://127.0.0.1:7890 17 | https_proxy = https://127.0.0.1:7890 18 | 19 | # 额外配置 20 | [Extra] 21 | # 忽略播放过的视频,(不加入合集) 22 | ignore_played = false 23 | emby_user_id = 0be793b82936460389544bb71b1d1f9c -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 根据豆瓣rss自动创建emby合集 2 | 3 | 参考 https://github.com/Baiganjia/EMBY_HotMovie_Importer 实现 4 | 5 | # 运行效果 6 | ![控制台](./docs/console_1.png) 7 | ![emby](./docs/emby_1.png) 8 | 9 | 需部署RSSHub 10 | ``` docker-compose 11 | version: '3' 12 | services: 13 | rsshub: 14 | image: xuanqb/rsshub:latest 15 | restart: unless-stopped 16 | ports: 17 | - 1200:1200 18 | environment: 19 | NODE_ENV: production 20 | container_name: rsshub 21 | ``` 22 | 23 | # 配置文件 24 | ``` conf 25 | [Server] 26 | # 这里填入你Emby服务器地址 27 | emby_server = http://xxx.xx.xx.x:8096 28 | # 这里填入你Emby API密钥 29 | emby_api_key = xxxxxxx 30 | rsshub_server = http://xx.xx.x.x:1200 31 | 32 | [Collection] 33 | #各种榜单 34 | rss_ids=movie_weekly_best,tv_global_best_weekly,tv_chinese_best_weekly,show_chinese_best_weekly,show_global_best_weekly,movie_hot_gaia,tv_hot,show_hot,movie_top250,subject_real_time_hotest 35 | ``` 36 | 37 | # 运行 38 | ``` python 39 | pip install -r requirements.txt 40 | python EMBY_HotMovie_Importer.py 41 | ``` 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/#use-with-ide 111 | .pdm.toml 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # PyCharm 157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 159 | # and can be added to the global gitignore or merged into this file. For a more nuclear 160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 161 | .idea/ 162 | 163 | -------------------------------------------------------------------------------- /EMBY_HotMovie_Importer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import urllib.parse 3 | from configparser import ConfigParser 4 | 5 | import requests 6 | import feedparser 7 | import re 8 | 9 | from typing import List 10 | 11 | config = ConfigParser() 12 | with open('config.conf', encoding='utf-8') as f: 13 | config.read_file(f) 14 | use_proxy = config.getboolean('Proxy', 'use_proxy', fallback=False) 15 | if use_proxy: 16 | os.environ['http_proxy'] = config.get('Proxy', 'http_proxy', fallback='http://127.0.0.1:7890') 17 | os.environ['https_proxy'] = config.get('Proxy', 'https_proxy', fallback='http://127.0.0.1:7890') 18 | else: 19 | os.environ.pop('http_proxy', None) 20 | os.environ.pop('https_proxy', None) 21 | 22 | 23 | class DbMovie: 24 | def __init__(self, name, year, type): 25 | self.name = name 26 | self.year = year 27 | self.type = type 28 | 29 | 30 | class DbMovieRss: 31 | def __init__(self, title, movies: List[DbMovie]): 32 | self.title = title 33 | self.movies = movies 34 | 35 | 36 | class EmbyBox: 37 | def __init__(self, box_id, box_movies): 38 | self.box_id = box_id 39 | self.box_movies = box_movies 40 | 41 | 42 | class Get_Detail(object): 43 | 44 | def __init__(self): 45 | self.noexist = [] 46 | self.dbmovies = {} 47 | 48 | # 获取配置项的值 49 | self.emby_server = config.get('Server', 'emby_server') 50 | self.emby_api_key = config.get('Server', 'emby_api_key') 51 | self.rsshub_server = config.get('Server', 'rsshub_server') 52 | self.ignore_played = config.getboolean('Extra', 'ignore_played', fallback=False) 53 | self.emby_user_id = config.get('Extra', 'emby_user_id', fallback='') 54 | self.rss_ids = config.get('Collection', 'rss_ids').split(',') 55 | 56 | self.headers = { 57 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36 Edg/101.0.1210.39" 58 | } 59 | 60 | def search_emby_by_name_and_year(self, db_movie: DbMovie): 61 | name = db_movie.name 62 | yearParam = f"&Years={db_movie.year}" 63 | includeItemTypes = "IncludeItemTypes=movie" 64 | ignore_played = "" 65 | emby_user_id = "" 66 | # 删除季信息 67 | if db_movie.type == "tv": 68 | yearParam = '' 69 | includeItemTypes = "IncludeItemTypes=Series" 70 | if self.ignore_played: 71 | # 不查询播放过的 72 | ignore_played = "&Filters=IsUnplayed" 73 | emby_user_id = f"Users/{self.emby_user_id}" 74 | url = f"{self.emby_server}/emby/{emby_user_id}/Items?api_key={self.emby_api_key}{ignore_played}&Recursive=true&{includeItemTypes}&SearchTerm={name}{yearParam}" 75 | response = requests.get(url) 76 | data = response.json() 77 | if response.status_code == 200 and data.get('TotalRecordCount', 0) > 0: 78 | for item in data.get('Items', []): 79 | if item['Name'] == name: 80 | return item 81 | return None 82 | else: 83 | return None 84 | 85 | def create_collection(self, collection_name, emby_id): 86 | encoded_collection_name = urllib.parse.quote(collection_name, safe='') 87 | url = f"{self.emby_server}/emby/Collections?IsLocked=false&Name={encoded_collection_name}&Ids={emby_id}&api_key={self.emby_api_key}" 88 | headers = { 89 | "accept": "application/json" 90 | } 91 | response = requests.post(url, headers=headers) 92 | if response.status_code == 200: 93 | collection_id = response.json().get('Id') 94 | print(f"成功创建合集: {collection_id}") 95 | return collection_id 96 | else: 97 | print("创建合集失败.") 98 | return None 99 | 100 | def add_movie_to_collection(self, emby_id, collection_id): 101 | url = f"{self.emby_server}/emby/Collections/{collection_id}/Items?Ids={emby_id}&api_key={self.emby_api_key}" 102 | headers = {"accept": "*/*"} 103 | response = requests.post(url, headers=headers) 104 | response.raise_for_status() 105 | return response.status_code == 204 106 | 107 | def check_collection_exists(self, collection_name) -> EmbyBox: 108 | encoded_collection_name = urllib.parse.quote(collection_name, safe='') 109 | url = f"{self.emby_server}/Items?IncludeItemTypes=BoxSet&Recursive=true&SearchTerm={encoded_collection_name}&api_key={self.emby_api_key}" 110 | response = requests.get(url) 111 | if response.status_code == 200: 112 | data = response.json() 113 | if len(data["Items"]) > 0 and data["Items"][0]["Type"] == "BoxSet": 114 | emby_box_id = data["Items"][0]['Id'] 115 | return EmbyBox(emby_box_id, self.get_emby_box_movie(emby_box_id)) 116 | return EmbyBox(None, []) 117 | 118 | def get_emby_box_movie(self, box_id): 119 | url = f"{self.emby_server}/emby/Items?api_key={self.emby_api_key}&ParentId={box_id}" 120 | response = requests.get(url) 121 | if response.status_code == 200: 122 | data = response.json() 123 | return [item["Name"] for item in data["Items"]] 124 | return [] 125 | 126 | def run(self): 127 | # 获取豆瓣rss 128 | for rss_id in self.rss_ids: 129 | self.dbmovies = self.get_douban_rss(rss_id) 130 | box_name = self.dbmovies.title 131 | print(f'更新 {box_name} rss_id:{rss_id}') 132 | # 如果存在返回id,否则返回'' 133 | emby_box = self.check_collection_exists(box_name) 134 | for db_movie in self.dbmovies.movies: 135 | box_id = emby_box.box_id 136 | movie_name = db_movie.name 137 | if movie_name in emby_box.box_movies: 138 | continue 139 | emby_data = self.search_emby_by_name_and_year(db_movie) 140 | if movie_name in self.noexist: 141 | continue 142 | elif emby_data and not box_id: 143 | emby_id = emby_data["Id"] 144 | emby_box.box_id = self.create_collection(box_name, emby_id) 145 | print(f"影视 '{movie_name}' 加入到合集成功.") 146 | elif emby_data: 147 | emby_id = emby_data["Id"] 148 | added_to_collection = self.add_movie_to_collection(emby_id, box_id) 149 | if added_to_collection: 150 | print(f"影视 '{movie_name}' 加入到合集成功.") 151 | else: 152 | print(f"影视 '{movie_name}' 加入到合集内失败.") 153 | else: 154 | self.noexist.append(movie_name) 155 | 156 | def get_douban_rss(self, rss_id): 157 | # 解析rss 158 | rss_url = f"{self.rsshub_server}/douban/movie/weekly/{rss_id}" 159 | # print(f"rss_url: {rss_url}") 160 | feed = feedparser.parse(rss_url) 161 | # 封装成对象 162 | movies = [] 163 | for item in feed.entries: 164 | name = item.title 165 | type = item.type 166 | if type == 'book': 167 | continue 168 | # 删除季信息 169 | if type == "tv": 170 | name = re.sub(r" 第[一二三四五六七八九十\d]+季", "", name) 171 | movies.append(DbMovie(name, item.year, type)) 172 | db_movie = DbMovieRss(feed.feed.title, movies) 173 | return db_movie 174 | 175 | 176 | if __name__ == "__main__": 177 | gd = Get_Detail() 178 | gd.run() 179 | --------------------------------------------------------------------------------