diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 81a2fe45..4887235d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,23 +8,20 @@ on: - version.py jobs: - build: + Docker-build: runs-on: ubuntu-latest name: Build Docker Image steps: - - - name: Checkout + - name: Checkout uses: actions/checkout@v4 - - - name: Release version + - name: Release version id: release_version run: | app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp") echo "app_version=$app_version" >> $GITHUB_ENV - - - name: Docker meta + - name: Docker Meta id: meta uses: docker/metadata-action@v5 with: @@ -33,23 +30,19 @@ jobs: type=raw,value=${{ env.app_version }} type=raw,value=latest - - - name: Set Up QEMU + - name: Set Up QEMU uses: docker/setup-qemu-action@v3 - - - name: Set Up Buildx + - name: Set Up Buildx uses: docker/setup-buildx-action@v3 - - - name: Login DockerHub + - name: Login DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build Image + - name: Build Image uses: docker/build-push-action@v5 with: context: . @@ -63,4 +56,77 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha, scope=${{ github.workflow }} - cache-to: type=gha, scope=${{ github.workflow }} \ No newline at end of file + cache-to: type=gha, scope=${{ github.workflow }} + + Windows-build: + runs-on: windows-latest + name: Build Windows Binary + steps: + - name: Checkout + uses: actions/checkout@v4 + + - 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 pyinstaller + pip install -r requirements.txt + shell: pwsh + + - name: Pyinstaller + run: | + pyinstaller windows.spec + shell: pwsh + + - name: Upload Windows File + uses: actions/upload-artifact@v3 + with: + name: windows + path: dist/MoviePilot.exe + + Create-release: + permissions: write-all + runs-on: ubuntu-latest + needs: [ Windows-build, Docker-build ] + steps: + - uses: actions/checkout@v2 + + - name: Release Version + id: release_version + run: | + app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp") + echo "app_version=$app_version" >> $GITHUB_ENV + + - name: Download Artifact + uses: actions/download-artifact@v3 + + - name: get release_informations + shell: bash + run: | + mkdir releases + mv ./windows/MoviePilot.exe ./releases/MoviePilot_v${{ env.app_version }}.exe + + - name: Create Release + id: create_release + uses: actions/create-release@latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ env.app_version }} + release_name: v${{ env.app_version }} + body: ${{ github.event.commits[0].message }} + draft: false + prerelease: false + + - name: Upload Release Asset + uses: dwenegar/upload-release-assets@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + release_id: ${{ steps.create_release.outputs.id }} + assets_path: | + ./releases/ \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 43c11bb5..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: MoviePilot Release -on: - workflow_dispatch: - push: - branches: - - main - paths: - - version.py - -jobs: - build: - runs-on: ubuntu-latest - name: Build Docker Image - steps: - - - name: Checkout - uses: actions/checkout@v4 - - - - name: Release Version - id: release_version - run: | - app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp") - echo "app_version=$app_version" >> $GITHUB_ENV - - - - name: Generate Release - uses: actions/create-release@latest - with: - tag_name: v${{ env.app_version }} - release_name: v${{ env.app_version }} - body: ${{ github.event.commits[0].message }} - draft: false - prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Dockerfile b/Dockerfile index cb52713d..5da36ae6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ FROM python:3.11.4-slim-bullseye ARG MOVIEPILOT_VERSION ENV LANG="C.UTF-8" \ + TZ="Asia/Shanghai" \ HOME="/moviepilot" \ TERM="xterm" \ PUID=0 \ diff --git a/README.md b/README.md index 2982c9a6..1f1c96f6 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ # 仅用于学习交流使用,请勿在任何国内平台宣传该项目! -Docker:https://hub.docker.com/r/jxxghp/moviepilot - 发布频道:https://t.me/moviepilot_channel ## 主要特性 @@ -33,11 +31,19 @@ MoviePilot需要配套下载器和媒体服务器配合使用。 ### 4. **安装MoviePilot** -目前仅提供docker镜像,点击 [这里](https://hub.docker.com/r/jxxghp/moviepilot) 或执行命令: +- Docker镜像 -```shell -docker pull jxxghp/moviepilot:latest -``` + 点击 [这里](https://hub.docker.com/r/jxxghp/moviepilot) 或执行命令: + + ```shell + docker pull jxxghp/moviepilot:latest + ``` + +- Window + + 后端:https://github.com/jxxghp/MoviePilot/releases + + 前端:https://github.com/jxxghp/MoviePilot-Frontend/releases ## 配置 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..e0c7de10 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) @@ -286,6 +296,6 @@ class Settings(BaseSettings): settings = Settings( - _env_file=Path(os.environ.get("CONFIG_DIR", "/config")) / "app.env", + _env_file=Settings().CONFIG_PATH / "app.env", _env_file_encoding="utf-8" ) 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/plugins/invitessignin/__init__.py b/app/plugins/invitessignin/__init__.py index 1a8d0528..f8bd39fd 100644 --- a/app/plugins/invitessignin/__init__.py +++ b/app/plugins/invitessignin/__init__.py @@ -22,7 +22,7 @@ class InvitesSignin(_PluginBase): # 插件图标 plugin_icon = "invites.png" # 主题色 - plugin_color = "#4FB647" + plugin_color = "#FFFFFF" # 插件版本 plugin_version = "1.0" # 插件作者 diff --git a/app/plugins/iyuuautoseed/__init__.py b/app/plugins/iyuuautoseed/__init__.py index bcfe01c2..9e5ff84f 100644 --- a/app/plugins/iyuuautoseed/__init__.py +++ b/app/plugins/iyuuautoseed/__init__.py @@ -109,7 +109,7 @@ class IYUUAutoSeed(_PluginBase): self._nolabels = config.get("nolabels") self._nopaths = config.get("nopaths") self._clearcache = config.get("clearcache") - self._permanent_error_caches = config.get("permanent_error_caches") or [] + self._permanent_error_caches = [] if self._clearcache else config.get("permanent_error_caches") or [] self._error_caches = [] if self._clearcache else config.get("error_caches") or [] self._success_caches = [] if self._clearcache else config.get("success_caches") or [] diff --git a/app/utils/system.py b/app/utils/system.py index e5a273aa..47cbe5f1 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 @@ -77,7 +97,7 @@ class SystemUtils: """ try: # link到当前目录并改名 - tmp_path = (src.parent / dest.name).with_suffix(".mp") + tmp_path = src.parent / (dest.name + ".mp") tmp_path.hardlink_to(src) # 移动到目标目录 shutil.move(tmp_path, dest) diff --git a/config/app.env b/config/app.env index d0e25c90..2a8ab338 100644 --- a/config/app.env +++ b/config/app.env @@ -3,10 +3,8 @@ ####################################################################### #################################### -# 系统设置 # +# 基础设置 # #################################### -# 时区 -TZ=Asia/Shanghai # 【*】API监听地址 HOST=0.0.0.0 # 是否调试模式 @@ -21,6 +19,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 +37,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 +183,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/version.py b/version.py index ae318a0f..e1d3dd99 100644 --- a/version.py +++ b/version.py @@ -1 +1 @@ -APP_VERSION = 'v1.2.9' +APP_VERSION = 'v1.3.0' diff --git a/windows.spec b/windows.spec new file mode 100644 index 00000000..62f1101b --- /dev/null +++ b/windows.spec @@ -0,0 +1,97 @@ +# -*- 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') + +block_cipher = None + +a = Analysis( + ['app/main.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, +) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + collect_pkg_data('config'), + collect_pkg_data('cf_clearance'), + collect_pkg_data('database', include_py_files=True), + [], + name='MoviePilot', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon="app.ico" +)