diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml new file mode 100644 index 00000000..129e10af --- /dev/null +++ b/.github/workflows/build-windows.yml @@ -0,0 +1,36 @@ +name: MoviePilot Windows Builder +on: + workflow_dispatch: + push: + branches: + - master + paths: + - version.py + - .github/workflows/build-windows.yml + +jobs: + Windows-build: + runs-on: windows-latest + steps: + - name: Init Python 3.11.4 + uses: actions/setup-python@v4 + with: + python-version: '3.11.4' + - name: Install Dependent Packages + run: | + python -m pip install --upgrade pip + pip install wheel numpy==1.23.5 pyparsing==3.0.9 wxpython==4.2.0 pyinstaller==5.7.0 + git clone --depth=1 -b main https://github.com/jxxghp/MoviePilot + cd MoviePilot + pip install -r requirements.txt + shell: pwsh + - name: Pyinstaller + run: | + cd MoviePilot + pyinstaller windows.spec + shell: pwsh + - name: Upload Windows File + uses: actions/upload-artifact@v3 + with: + name: windows + path: MoviePilot/dist/MoviePilot.exe diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 81a2fe45..affd37c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: MoviePilot Builder +name: MoviePilot Docker Builder on: workflow_dispatch: push: diff --git a/app.ico b/app.ico new file mode 100644 index 00000000..18d99480 Binary files /dev/null and b/app.ico differ diff --git a/app/core/config.py b/app/core/config.py index 2d720370..8bf7dd84 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,10 +1,13 @@ import os import secrets +import sys from pathlib import Path from typing import List from pydantic import BaseSettings +from app.utils.system import SystemUtils + class Settings(BaseSettings): # 项目名称 @@ -28,7 +31,7 @@ class Settings(BaseSettings): # 是否开发模式 DEV: bool = False # 配置文件目录 - CONFIG_DIR: str = "/config" + CONFIG_DIR: str = None # 超级管理员 SUPERUSER: str = "admin" # 超级管理员初始密码 @@ -209,7 +212,11 @@ class Settings(BaseSettings): def CONFIG_PATH(self): if self.CONFIG_DIR: return Path(self.CONFIG_DIR) - return self.INNER_CONFIG_PATH + elif SystemUtils.is_docker(): + return Path("/config") + elif SystemUtils.is_frozen(): + return Path(sys.executable).parent / "config" + return self.ROOT_PATH / "config" @property def TEMP_PATH(self): @@ -274,6 +281,9 @@ class Settings(BaseSettings): with self.CONFIG_PATH as p: if not p.exists(): p.mkdir(parents=True, exist_ok=True) + if SystemUtils.is_frozen(): + if not (p / "app.env").exists(): + SystemUtils.copy(self.INNER_CONFIG_PATH / "app.env", p / "app.env") with self.TEMP_PATH as p: if not p.exists(): p.mkdir(parents=True, exist_ok=True) diff --git a/app/db/init.py b/app/db/init.py index c902c1fb..d5ee24b5 100644 --- a/app/db/init.py +++ b/app/db/init.py @@ -39,7 +39,7 @@ def update_db(): 更新数据库 """ db_location = settings.CONFIG_PATH / 'user.db' - script_location = settings.ROOT_PATH / 'alembic' + script_location = settings.ROOT_PATH / 'database' try: alembic_cfg = Config() alembic_cfg.set_main_option('script_location', str(script_location)) diff --git a/app/helper/display.py b/app/helper/display.py index 1ae6504a..06fb412b 100644 --- a/app/helper/display.py +++ b/app/helper/display.py @@ -2,12 +2,15 @@ from pyvirtualdisplay import Display from app.log import logger from app.utils.singleton import Singleton +from app.utils.system import SystemUtils class DisplayHelper(metaclass=Singleton): _display: Display = None def __init__(self): + if not SystemUtils.is_docker(): + return try: self._display = Display(visible=False, size=(1024, 768)) self._display.start() diff --git a/app/utils/system.py b/app/utils/system.py index e5a273aa..10243801 100644 --- a/app/utils/system.py +++ b/app/utils/system.py @@ -3,6 +3,7 @@ import os import platform import re import shutil +import sys from pathlib import Path from typing import List, Union, Tuple @@ -27,20 +28,39 @@ class SystemUtils: @staticmethod def is_docker() -> bool: + """ + 判断是否为Docker环境 + """ return Path("/.dockerenv").exists() @staticmethod def is_synology() -> bool: + """ + 判断是否为群晖系统 + """ if SystemUtils.is_windows(): return False return True if "synology" in SystemUtils.execute('uname -a') else False @staticmethod def is_windows() -> bool: + """ + 判断是否为Windows系统 + """ return True if os.name == "nt" else False + @staticmethod + def is_frozen() -> bool: + """ + 判断是否为冻结的二进制文件 + """ + return True if getattr(sys, 'frozen', False) else False + @staticmethod def is_macos() -> bool: + """ + 判断是否为MacOS系统 + """ return True if platform.system() == 'Darwin' else False @staticmethod diff --git a/config/app.env b/config/app.env index d0e25c90..4363817e 100644 --- a/config/app.env +++ b/config/app.env @@ -3,7 +3,7 @@ ####################################################################### #################################### -# 系统设置 # +# 基础设置 # #################################### # 时区 TZ=Asia/Shanghai @@ -21,6 +21,10 @@ SUPERUSER_PASSWORD=password API_TOKEN=moviepilot # 网络代理 IP:PORT PROXY_HOST= +# TMDB图片地址,无需修改需保留默认值 +TMDB_IMAGE_DOMAIN=image.tmdb.org +# TMDB API地址,无需修改需保留默认值 +TMDB_API_DOMAIN=api.themoviedb.org # 大内存模式 BIG_MEMORY_MODE=false @@ -35,13 +39,45 @@ SCRAP_METADATA=true SCRAP_FOLLOW_TMDB=true # 刮削来源 themoviedb/douban SCRAP_SOURCE=themoviedb -# TMDB图片地址,无需修改需保留默认值 -TMDB_IMAGE_DOMAIN=image.tmdb.org -# TMDB API地址,无需修改需保留默认值 -TMDB_API_DOMAIN=api.themoviedb.org #################################### -# 订阅&搜索 # +# 媒体库 # +#################################### +# 【*】转移方式 link/copy/move/softlink +TRANSFER_TYPE=copy +# 【*】媒体库目录,多个目录使用,分隔 +LIBRARY_PATH= +# 电影媒体库目录名,默认电影 +LIBRARY_MOVIE_NAME= +# 电视剧媒体库目录名,默认电视剧 +LIBRARY_TV_NAME= +# 动漫媒体库目录名,默认电视剧/动漫 +LIBRARY_ANIME_NAME= +# 二级分类 +LIBRARY_CATEGORY=true +# 电影重命名格式 +MOVIE_RENAME_FORMAT={{title}}{% if year %} ({{year}}){% endif %}/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}{{fileExt}} +# 电视剧重命名格式 +TV_RENAME_FORMAT={{title}}{% if year %} ({{year}}){% endif %}/Season {{season}}/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}{{fileExt}} + +#################################### +# 站点 # +#################################### +# 【*】CookieCloud服务器地址,默认为公共服务器 +COOKIECLOUD_HOST=https://movie-pilot.org/cookiecloud +# 【*】CookieCloud用户KEY +COOKIECLOUD_KEY= +# 【*】CookieCloud端对端加密密码 +COOKIECLOUD_PASSWORD= +# 【*】CookieCloud同步间隔(分钟) +COOKIECLOUD_INTERVAL=1440 +# OCR服务器地址 +OCR_HOST=https://movie-pilot.org +# 【*】CookieCloud对应的浏览器UA +USER_AGENT=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57 + +#################################### +# 订阅 & 搜索 # #################################### # 订阅模式 spider/rss SUBSCRIBE_MODE=spider @@ -149,39 +185,3 @@ JELLYFIN_API_KEY= PLEX_HOST= # Plex Token PLEX_TOKEN= - -#################################### -# 站点 # -#################################### -# 【*】CookieCloud服务器地址,默认为公共服务器 -COOKIECLOUD_HOST=https://movie-pilot.org/cookiecloud -# 【*】CookieCloud用户KEY -COOKIECLOUD_KEY= -# 【*】CookieCloud端对端加密密码 -COOKIECLOUD_PASSWORD= -# 【*】CookieCloud同步间隔(分钟) -COOKIECLOUD_INTERVAL=1440 -# OCR服务器地址 -OCR_HOST=https://movie-pilot.org -# 【*】CookieCloud对应的浏览器UA -USER_AGENT=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57 - -#################################### -# 媒体库 # -#################################### -# 【*】转移方式 link/copy/move/softlink -TRANSFER_TYPE=copy -# 【*】媒体库目录,多个目录使用,分隔 -LIBRARY_PATH= -# 电影媒体库目录名,默认电影 -LIBRARY_MOVIE_NAME= -# 电视剧媒体库目录名,默认电视剧 -LIBRARY_TV_NAME= -# 动漫媒体库目录名,默认电视剧/动漫 -LIBRARY_ANIME_NAME= -# 二级分类 -LIBRARY_CATEGORY=true -# 电影重命名格式 -MOVIE_RENAME_FORMAT={{title}}{% if year %} ({{year}}){% endif %}/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}{{fileExt}} -# 电视剧重命名格式 -TV_RENAME_FORMAT={{title}}{% if year %} ({{year}}){% endif %}/Season {{season}}/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}{{fileExt}} diff --git a/alembic/env.py b/database/env.py similarity index 100% rename from alembic/env.py rename to database/env.py diff --git a/alembic/gen.py b/database/gen.py similarity index 92% rename from alembic/gen.py rename to database/gen.py index 876b8525..9bf4edca 100644 --- a/alembic/gen.py +++ b/database/gen.py @@ -12,7 +12,7 @@ for module in Path(__file__).with_name("models").glob("*.py"): db_version = input("请输入版本号:") db_location = settings.CONFIG_PATH / 'user.db' -script_location = settings.ROOT_PATH / 'alembic' +script_location = settings.ROOT_PATH / 'database' alembic_cfg = AlembicConfig() alembic_cfg.set_main_option('script_location', str(script_location)) alembic_cfg.set_main_option('sqlalchemy.url', f"sqlite:///{db_location}") diff --git a/alembic/script.py.mako b/database/script.py.mako similarity index 100% rename from alembic/script.py.mako rename to database/script.py.mako diff --git a/alembic/versions/14f1813ae8e3_1_0_1.py b/database/versions/14f1813ae8e3_1_0_1.py similarity index 100% rename from alembic/versions/14f1813ae8e3_1_0_1.py rename to database/versions/14f1813ae8e3_1_0_1.py diff --git a/alembic/versions/1e169250e949_1_0_4.py b/database/versions/1e169250e949_1_0_4.py similarity index 100% rename from alembic/versions/1e169250e949_1_0_4.py rename to database/versions/1e169250e949_1_0_4.py diff --git a/alembic/versions/232dfa044617_1_0_6.py b/database/versions/232dfa044617_1_0_6.py similarity index 100% rename from alembic/versions/232dfa044617_1_0_6.py rename to database/versions/232dfa044617_1_0_6.py diff --git a/alembic/versions/30329639c12b_1_0_7.py b/database/versions/30329639c12b_1_0_7.py similarity index 100% rename from alembic/versions/30329639c12b_1_0_7.py rename to database/versions/30329639c12b_1_0_7.py diff --git a/alembic/versions/52ab4930be04_1_0_3.py b/database/versions/52ab4930be04_1_0_3.py similarity index 100% rename from alembic/versions/52ab4930be04_1_0_3.py rename to database/versions/52ab4930be04_1_0_3.py diff --git a/alembic/versions/9f4edd55c2d4_1_0_0.py b/database/versions/9f4edd55c2d4_1_0_0.py similarity index 100% rename from alembic/versions/9f4edd55c2d4_1_0_0.py rename to database/versions/9f4edd55c2d4_1_0_0.py diff --git a/alembic/versions/a521fbc28b18_1_0_9.py b/database/versions/a521fbc28b18_1_0_9.py similarity index 100% rename from alembic/versions/a521fbc28b18_1_0_9.py rename to database/versions/a521fbc28b18_1_0_9.py diff --git a/alembic/versions/b2f011d3a8b7_1_0_8.py b/database/versions/b2f011d3a8b7_1_0_8.py similarity index 100% rename from alembic/versions/b2f011d3a8b7_1_0_8.py rename to database/versions/b2f011d3a8b7_1_0_8.py diff --git a/alembic/versions/e734c7fe6056_1_0_5.py b/database/versions/e734c7fe6056_1_0_5.py similarity index 100% rename from alembic/versions/e734c7fe6056_1_0_5.py rename to database/versions/e734c7fe6056_1_0_5.py diff --git a/alembic/versions/ec5fb51fc300_1_0_2.py b/database/versions/ec5fb51fc300_1_0_2.py similarity index 100% rename from alembic/versions/ec5fb51fc300_1_0_2.py rename to database/versions/ec5fb51fc300_1_0_2.py diff --git a/windows.spec b/windows.spec new file mode 100644 index 00000000..ed451618 --- /dev/null +++ b/windows.spec @@ -0,0 +1,101 @@ +# -*- mode: python ; coding: utf-8 -*- + +def collect_pkg_data(package, include_py_files=False, subdir=None): + """ + Collect all data files from the given package. + """ + import os + from PyInstaller.utils.hooks import get_package_paths, remove_prefix, PY_IGNORE_EXTENSIONS + + # Accept only strings as packages. + if type(package) is not str: + raise ValueError + + pkg_base, pkg_dir = get_package_paths(package) + if subdir: + pkg_dir = os.path.join(pkg_dir, subdir) + # Walk through all file in the given package, looking for data files. + data_toc = TOC() + for dir_path, dir_names, files in os.walk(pkg_dir): + for f in files: + extension = os.path.splitext(f)[1] + if include_py_files or (extension not in PY_IGNORE_EXTENSIONS): + source_file = os.path.join(dir_path, f) + dest_folder = remove_prefix(dir_path, os.path.dirname(pkg_base) + os.sep) + dest_file = os.path.join(dest_folder, f) + data_toc.append((dest_file, source_file, 'DATA')) + return data_toc + + +def collect_local_submodules(package): + """ + Collect all local submodules from the given package. + """ + import os + base_dir = '..' + package_dir = os.path.join(base_dir, package.replace('.', os.sep)) + submodules = [] + for dir_path, dir_names, files in os.walk(package_dir): + for f in files: + if f == '__init__.py': + submodules.append(f"{package}.{os.path.basename(dir_path)}") + elif f.endswith('.py'): + submodules.append(f"{package}.{os.path.basename(dir_path)}.{os.path.splitext(f)[0]}") + for d in dir_names: + submodules.append(f"{package}.{os.path.basename(dir_path)}.{d}") + return submodules + + +hiddenimports = [ + 'passlib.handlers.bcrypt', + 'app.modules', + 'app.plugins', +] + collect_local_submodules('app.modules') \ + + collect_local_submodules('app.plugins') + +a = Analysis( + ['app/main.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, +) + +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='MoviePilot', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon="app.ico" +) + +coll = COLLECT( + exe, + a.binaries, + a.datas, + collect_pkg_data('config'), + collect_pkg_data('cf_clearance'), + collect_pkg_data('database', include_py_files=True), + strip=False, + upx=True, + upx_exclude=[], + name='MoviePilot', +)