├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ └── feature.md └── workflows │ ├── build-beta.yml │ ├── build-lite.yml │ ├── build-windows.yml │ └── build.yml ├── .gitignore ├── .gitmodules ├── LICENSE.md ├── README.md ├── app ├── __init__.py ├── brushtask.py ├── conf │ ├── __init__.py │ ├── moduleconf.py │ ├── siteconf.py │ └── systemconfig.py ├── db │ ├── __init__.py │ ├── main_db.py │ ├── media_db.py │ └── models.py ├── doubansync.py ├── downloader │ ├── __init__.py │ ├── client │ │ ├── __init__.py │ │ ├── _base.py │ │ ├── _py115.py │ │ ├── _pyaria2.py │ │ ├── aria2.py │ │ ├── client115.py │ │ ├── pikpak.py │ │ ├── qbittorrent.py │ │ └── transmission.py │ └── downloader.py ├── filetransfer.py ├── filter.py ├── helper │ ├── __init__.py │ ├── chrome_helper.py │ ├── cookiecloud_helper.py │ ├── db_helper.py │ ├── dict_helper.py │ ├── display_helper.py │ ├── ffmpeg_helper.py │ ├── indexer_helper.py │ ├── meta_helper.py │ ├── ocr_helper.py │ ├── opensubtitles.py │ ├── progress_helper.py │ ├── security_helper.py │ ├── site_helper.py │ ├── submodule_helper.py │ ├── thread_helper.py │ └── words_helper.py ├── indexer │ ├── __init__.py │ ├── client │ │ ├── __init__.py │ │ ├── _base.py │ │ ├── _rarbg.py │ │ ├── _render_spider.py │ │ ├── _spider.py │ │ ├── _tnode.py │ │ ├── builtin.py │ │ ├── jackett.py │ │ └── prowlarr.py │ └── indexer.py ├── media │ ├── __init__.py │ ├── bangumi.py │ ├── category.py │ ├── douban.py │ ├── doubanapi │ │ ├── __init__.py │ │ ├── apiv2.py │ │ └── webapi.py │ ├── fanart.py │ ├── media.py │ ├── meta │ │ ├── __init__.py │ │ ├── _base.py │ │ ├── metaanime.py │ │ ├── metainfo.py │ │ ├── metavideo.py │ │ └── release_groups.py │ ├── scraper.py │ └── tmdbv3api │ │ ├── __init__.py │ │ ├── as_obj.py │ │ ├── exceptions.py │ │ ├── objs │ │ ├── __init__.py │ │ ├── discover.py │ │ ├── episode.py │ │ ├── find.py │ │ ├── genre.py │ │ ├── movie.py │ │ ├── person.py │ │ ├── search.py │ │ ├── trending.py │ │ └── tv.py │ │ └── tmdb.py ├── mediaserver │ ├── __init__.py │ ├── client │ │ ├── __init__.py │ │ ├── _base.py │ │ ├── emby.py │ │ ├── jellyfin.py │ │ └── plex.py │ ├── media_server.py │ └── webhook_event.py ├── message │ ├── __init__.py │ ├── client │ │ ├── __init__.py │ │ ├── _base.py │ │ ├── bark.py │ │ ├── chanify.py │ │ ├── gotify.py │ │ ├── iyuu.py │ │ ├── pushdeer.py │ │ ├── pushplus.py │ │ ├── serverchan.py │ │ ├── slack.py │ │ ├── synologychat.py │ │ ├── telegram.py │ │ └── wechat.py │ ├── message.py │ └── message_center.py ├── rss.py ├── rsschecker.py ├── scheduler.py ├── searcher.py ├── sites │ ├── __init__.py │ ├── site_user_info_factory.py │ ├── sitecookie.py │ ├── sites.py │ └── siteuserinfo │ │ ├── __init__.py │ │ ├── _base.py │ │ ├── discuz.py │ │ ├── file_list.py │ │ ├── gazelle.py │ │ ├── ipt_project.py │ │ ├── nexus_php.py │ │ ├── nexus_project.py │ │ ├── nexus_rabbit.py │ │ ├── small_horse.py │ │ ├── tnode.py │ │ ├── torrent_leech.py │ │ └── unit3d.py ├── speedlimiter.py ├── subscribe.py ├── subtitle.py ├── sync.py ├── torrentremover.py └── utils │ ├── __init__.py │ ├── cache_manager.py │ ├── commons.py │ ├── dom_utils.py │ ├── episode_format.py │ ├── exception_utils.py │ ├── http_utils.py │ ├── json_utils.py │ ├── number_utils.py │ ├── path_utils.py │ ├── rsstitle_utils.py │ ├── string_utils.py │ ├── system_utils.py │ ├── tokens.py │ ├── torrent.py │ └── types.py ├── build_sites.py ├── check_config.py ├── config.py ├── config ├── config.yaml ├── default-category.yaml ├── init_filter.sql ├── init_userrss_v3.sql ├── reset_db_version.sql ├── sites.dat ├── update_subscribe.sql ├── update_userpris.sql └── update_userrss.sql ├── db_scripts ├── README ├── env.py ├── script.py.mako └── versions │ └── 720a6289a697_1_1_0.py ├── dbscript_gen.py ├── docker ├── Dockerfile ├── Dockerfile.beta ├── Dockerfile.lite ├── compose.yml ├── entrypoint.sh ├── readme.md └── volume.png ├── log.py ├── package_list.txt ├── requirements.txt ├── run.py ├── tests ├── __init__.py ├── cases │ ├── __init__.py │ └── meta_cases.py ├── run.py └── test_metainfo.py ├── third_party.txt ├── version.py ├── web ├── __init__.py ├── action.py ├── apiv1.py ├── backend │ ├── WXBizMsgCrypt3.py │ ├── __init__.py │ ├── search_torrents.py │ ├── user.py │ ├── wallpaper.py │ └── web_utils.py ├── main.py ├── robots.txt ├── security.py ├── static │ ├── components │ │ ├── card │ │ │ ├── index.js │ │ │ ├── normal │ │ │ │ ├── index.js │ │ │ │ ├── placeholder.js │ │ │ │ └── state.js │ │ │ └── person │ │ │ │ └── index.js │ │ ├── custom │ │ │ ├── chips │ │ │ │ └── index.html │ │ │ ├── img │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ └── slide │ │ │ │ └── index.js │ │ ├── index.js │ │ ├── layout │ │ │ ├── index.js │ │ │ ├── navbar │ │ │ │ ├── button.js │ │ │ │ └── index.js │ │ │ └── searchbar │ │ │ │ └── index.js │ │ ├── lit-index.js │ │ ├── page │ │ │ ├── discovery │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ ├── mediainfo │ │ │ │ └── index.js │ │ │ └── person │ │ │ │ └── index.js │ │ └── utility │ │ │ ├── lit-core.min.js │ │ │ ├── lit-state.js │ │ │ └── utility.js │ ├── css │ │ ├── demo.min.css │ │ ├── dropzone.css │ │ ├── font-awesome.min.css │ │ ├── fullcalendar.min.css │ │ ├── jquery.filetree.css │ │ ├── jsoneditor.min.css │ │ ├── nprogress.css │ │ ├── style.css │ │ └── tabler.min.css │ ├── favicon.ico │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── img │ │ ├── 115.jpg │ │ ├── aria2.png │ │ ├── bark.webp │ │ ├── bug_fixing.svg │ │ ├── chanify.png │ │ ├── chinesesubfinder.png │ │ ├── emby.png │ │ ├── filetree │ │ │ ├── application.png │ │ │ ├── code.png │ │ │ ├── css.png │ │ │ ├── db.png │ │ │ ├── directory-lock.png │ │ │ ├── directory.png │ │ │ ├── doc.png │ │ │ ├── file-lock.png │ │ │ ├── file.png │ │ │ ├── film.png │ │ │ ├── flash.png │ │ │ ├── folder_open.png │ │ │ ├── html.png │ │ │ ├── java.png │ │ │ ├── linux.png │ │ │ ├── music.png │ │ │ ├── pdf.png │ │ │ ├── php.png │ │ │ ├── picture.png │ │ │ ├── ppt.png │ │ │ ├── psd.png │ │ │ ├── ruby.png │ │ │ ├── script.png │ │ │ ├── spinner.gif │ │ │ ├── txt.png │ │ │ ├── xls.png │ │ │ └── zip.png │ │ ├── gotify.png │ │ ├── icon-imdb.png │ │ ├── icons │ │ │ ├── 1024.png │ │ │ ├── 128.png │ │ │ ├── 144.png │ │ │ ├── 152.png │ │ │ ├── 167.png │ │ │ ├── 172.png │ │ │ ├── 180.png │ │ │ ├── 196.png │ │ │ ├── 196_ALT.png │ │ │ ├── 216.png │ │ │ ├── 256.png │ │ │ ├── 512.png │ │ │ └── 512_ALT.png │ │ ├── indexer.jpg │ │ ├── indexer.png │ │ ├── iyuu.png │ │ ├── jackett.png │ │ ├── jellyfin.jpg │ │ ├── jellyfin.png │ │ ├── joyride.svg │ │ ├── logo-16x16.png │ │ ├── logo-32x32.png │ │ ├── logo-black.png │ │ ├── logo-blue.png │ │ ├── logo-white.png │ │ ├── logo.png │ │ ├── medicine.svg │ │ ├── mobile_application.svg │ │ ├── movie.jpg │ │ ├── music.png │ │ ├── no-image.png │ │ ├── opensubtitles.png │ │ ├── person.png │ │ ├── pikpak.png │ │ ├── plex.png │ │ ├── posting_photo.svg │ │ ├── printing_invoices.svg │ │ ├── prowlarr.png │ │ ├── pt.jpg │ │ ├── pushdeer.png │ │ ├── pushplus.jpg │ │ ├── qbittorrent.png │ │ ├── quitting_time.svg │ │ ├── serverchan.png │ │ ├── sign_in.svg │ │ ├── slack.png │ │ ├── splash │ │ │ ├── apple-splash-1125-2436.png │ │ │ ├── apple-splash-1136-640.png │ │ │ ├── apple-splash-1170-2532.png │ │ │ ├── apple-splash-1242-2208.png │ │ │ ├── apple-splash-1242-2688.png │ │ │ ├── apple-splash-1284-2778.png │ │ │ ├── apple-splash-1334-750.png │ │ │ ├── apple-splash-1536-2048.png │ │ │ ├── apple-splash-1620-2160.png │ │ │ ├── apple-splash-1668-2224.png │ │ │ ├── apple-splash-1668-2388.png │ │ │ ├── apple-splash-1792-828.png │ │ │ ├── apple-splash-2048-1536.png │ │ │ ├── apple-splash-2048-2732.png │ │ │ ├── apple-splash-2160-1620.png │ │ │ ├── apple-splash-2208-1242.png │ │ │ ├── apple-splash-2224-1668.png │ │ │ ├── apple-splash-2388-1668.png │ │ │ ├── apple-splash-2436-1125.png │ │ │ ├── apple-splash-2532-1170.png │ │ │ ├── apple-splash-2688-1242.png │ │ │ ├── apple-splash-2732-2048.png │ │ │ ├── apple-splash-2778-1284.png │ │ │ ├── apple-splash-640-1136.png │ │ │ ├── apple-splash-750-1334.png │ │ │ └── apple-splash-828-1792.png │ │ ├── startup.jpg │ │ ├── synologychat.png │ │ ├── telegram.png │ │ ├── tmdb.png │ │ ├── tmdb.webp │ │ ├── transmission.png │ │ ├── tv.png │ │ ├── users.png │ │ ├── wechat.png │ │ └── work_together.svg │ ├── js │ │ ├── FileSaver.min.js │ │ ├── ace.js │ │ ├── demo-theme.min.js │ │ ├── demo.min.js │ │ ├── dom-to-image.min.js │ │ ├── dropzone-min.js │ │ ├── echarts.min.js │ │ ├── fullcalendar.min.js │ │ ├── jquery-3.3.1.min.js │ │ ├── jquery.filetree.js │ │ ├── libs │ │ │ └── list.min.js │ │ ├── locales │ │ │ └── zh-cn.js │ │ ├── mode-css.js │ │ ├── mode-javascript.js │ │ ├── mode-json.js │ │ ├── nprogress.js │ │ ├── numeral.min.js │ │ ├── tabler.min.js │ │ ├── theme-one_dark.js │ │ ├── theme-xcode.js │ │ ├── util.js │ │ ├── worker-css.js │ │ ├── worker-javascript.js │ │ └── worker-json.js │ └── site.webmanifest └── templates │ ├── 404.html │ ├── 500.html │ ├── discovery │ ├── mediainfo.html │ ├── person.html │ ├── ranking.html │ └── recommend.html │ ├── download │ ├── downloading.html │ ├── torrent_remove.html │ └── userdownloader.html │ ├── index.html │ ├── login.html │ ├── macro │ ├── form.html │ ├── head.html │ ├── oops.html │ └── svg.html │ ├── navigation.html │ ├── rename │ ├── history.html │ ├── mediafile.html │ ├── tmdbcache.html │ └── unidentification.html │ ├── rss │ ├── movie_rss.html │ ├── rss_calendar.html │ ├── rss_history.html │ ├── rss_parser.html │ ├── tv_rss.html │ └── user_rss.html │ ├── search.html │ ├── service.html │ ├── setting │ ├── basic.html │ ├── customwords.html │ ├── directorysync.html │ ├── douban.html │ ├── download_setting.html │ ├── downloader.html │ ├── filterrule.html │ ├── indexer.html │ ├── library.html │ ├── mediaserver.html │ ├── notification.html │ ├── subtitle.html │ └── users.html │ ├── site │ ├── brushtask.html │ ├── resources.html │ ├── site.html │ ├── sitelist.html │ └── statistics.html │ └── test.html └── windows ├── nas-tools.ico ├── nas-tools.spec ├── rely ├── hook-cn2an.py ├── hook-zhconv.py ├── template.jinja2 └── upx.exe └── trayicon.py /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 问题模板 3 | about: 如发现Bug,请按此模板提交issues,不按模板提交的问题将直接关闭。 4 | 提交问题务必描述清楚、附上日志,描述不清导致无法理解和分析的问题也可能会被直接关闭。 5 | --- 6 | 7 | ## 你使用的 NAStool 是什么版本,什么环境? 8 | 9 | > NAStool 版本: vx.x.x 10 | > 11 | > 环境: docker or windows or Synology 12 | > 13 | 14 | ## 你遇到什么问题了? 15 | 16 | > 描述一下你遇到的问题 17 | 18 | ## 是否已经浏览过Issues、Wiki及TG公众号仍无法解决? 19 | 20 | > 请搜索Issues列表、查看wiki跟TG公众号的更新说明,已经解释过的问题不要重复提问 21 | 22 | 23 | ## 你期望的结果 24 | 25 | > 描述以下你期望的结果 26 | 27 | ## 给出程序界面截图、后台运行日志或配置文件 28 | 29 | > 如UI BUG请提供截图及配置文件截图 30 | > 其它问题提供后台日志,如为Docker请提供docker的日志 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能需求模板 3 | about: 如有新功能需要需要提交,请按此模板创建issues 4 | --- 5 | 6 | ## 你使用的 NAStool 是什么版本,什么环境? 7 | 8 | > NAStool 版本: vx.x.x 9 | > 10 | > 环境: docker or windows or synology 11 | 12 | ## 你想要新增或者改进什么功能? 13 | 14 | > 你想要新增或者改进什么功能? 15 | 16 | ## 这个功能有什么可以参考的资料吗? 17 | 18 | > 这个功能有什么可以参考的资料吗?是否可以列举一些,不要引用同类但商业化软件的任何内容. 19 | -------------------------------------------------------------------------------- /.github/workflows/build-beta.yml: -------------------------------------------------------------------------------- 1 | name: Build NAStool Beta Image 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - dev 7 | paths: 8 | - version.py 9 | - docker/Dockerfile.beta 10 | - .github/workflows/build-beta.yml 11 | - requirements.txt 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | name: Build Docker Image 16 | steps: 17 | - 18 | name: Checkout 19 | uses: actions/checkout@master 20 | 21 | - 22 | name: Release version 23 | id: release_version 24 | run: | 25 | app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp") 26 | echo "app_version=$app_version" >> $GITHUB_ENV 27 | 28 | - 29 | name: Set Up QEMU 30 | uses: docker/setup-qemu-action@v1 31 | 32 | - 33 | name: Set Up Buildx 34 | uses: docker/setup-buildx-action@v1 35 | 36 | - 37 | name: Login DockerHub 38 | uses: docker/login-action@v1 39 | with: 40 | username: ${{ secrets.DOCKER_USERNAME }} 41 | password: ${{ secrets.DOCKER_PASSWORD }} 42 | 43 | - 44 | name: Buildx 45 | uses: docker/build-push-action@v2 46 | with: 47 | context: . 48 | file: docker/Dockerfile.beta 49 | platforms: | 50 | linux/amd64 51 | linux/arm64 52 | push: true 53 | tags: | 54 | ${{ secrets.DOCKER_USERNAME }}/nas-tools:${{ env.app_version }}-beta -------------------------------------------------------------------------------- /.github/workflows/build-lite.yml: -------------------------------------------------------------------------------- 1 | name: Build NAStool Lite Image 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - version.py 9 | - docker/Dockerfile.lite 10 | - .github/workflows/build-lite.yml 11 | - requirements.txt 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | name: Build Docker Image 16 | steps: 17 | - 18 | name: Checkout 19 | uses: actions/checkout@master 20 | 21 | - 22 | name: Release version 23 | id: release_version 24 | run: | 25 | app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp") 26 | echo "app_version=$app_version" >> $GITHUB_ENV 27 | 28 | - 29 | name: Set Up QEMU 30 | uses: docker/setup-qemu-action@v1 31 | 32 | - 33 | name: Set Up Buildx 34 | uses: docker/setup-buildx-action@v1 35 | 36 | - 37 | name: Login DockerHub 38 | uses: docker/login-action@v1 39 | with: 40 | username: ${{ secrets.DOCKER_USERNAME }} 41 | password: ${{ secrets.DOCKER_PASSWORD }} 42 | 43 | - 44 | name: Build Lite Image 45 | uses: docker/build-push-action@v2 46 | with: 47 | context: . 48 | file: docker/Dockerfile.lite 49 | platforms: | 50 | linux/amd64 51 | linux/arm64 52 | push: true 53 | tags: | 54 | ${{ secrets.DOCKER_USERNAME }}/nas-tools:${{ env.app_version }}-lite -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build NAStool Image 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - version.py 9 | - docker/Dockerfile 10 | - docker/Dockerfile.lite 11 | - .github/workflows/build.yml 12 | - requirements.txt 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | name: Build Docker Image 17 | steps: 18 | - 19 | name: Checkout 20 | uses: actions/checkout@master 21 | 22 | - 23 | name: Release version 24 | id: release_version 25 | run: | 26 | app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp") 27 | echo "app_version=$app_version" >> $GITHUB_ENV 28 | 29 | - 30 | name: Set Up QEMU 31 | uses: docker/setup-qemu-action@v1 32 | 33 | - 34 | name: Set Up Buildx 35 | uses: docker/setup-buildx-action@v1 36 | 37 | - 38 | name: Login DockerHub 39 | uses: docker/login-action@v1 40 | with: 41 | username: ${{ secrets.DOCKER_USERNAME }} 42 | password: ${{ secrets.DOCKER_PASSWORD }} 43 | 44 | - name: Build Image 45 | uses: docker/build-push-action@v2 46 | with: 47 | context: . 48 | file: docker/Dockerfile 49 | platforms: | 50 | linux/amd64 51 | linux/arm64 52 | push: true 53 | tags: | 54 | ${{ secrets.DOCKER_USERNAME }}/nas-tools:latest 55 | ${{ secrets.DOCKER_USERNAME }}/nas-tools:${{ env.app_version }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.sock 3 | *.log 4 | *.pid 5 | test.py 6 | 7 | ### IntelliJ IDEA ### 8 | .idea 9 | *.iws 10 | *.iml 11 | *.ipr 12 | out/ 13 | gen/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_party/qbittorrent-api"] 2 | path = third_party/qbittorrent-api 3 | url = https://github.com/rmartin16/qbittorrent-api 4 | [submodule "third_party/transmission-rpc"] 5 | path = third_party/transmission-rpc 6 | url = https://github.com/Trim21/transmission-rpc 7 | [submodule "third_party/anitopy"] 8 | path = third_party/anitopy 9 | url = https://github.com/igorcmoura/anitopy 10 | [submodule "third_party/plexapi"] 11 | path = third_party/plexapi 12 | url = https://github.com/pkkid/python-plexapi 13 | [submodule "third_party/slack_bolt"] 14 | path = third_party/slack_bolt 15 | url = https://github.com/slackapi/bolt-python 16 | [submodule "third_party/feapder"] 17 | path = third_party/feapder 18 | url = https://github.com/jxxghp/feapder 19 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/app/__init__.py -------------------------------------------------------------------------------- /app/conf/__init__.py: -------------------------------------------------------------------------------- 1 | from .systemconfig import SystemConfig 2 | from .moduleconf import ModuleConf 3 | from .siteconf import SiteConf 4 | -------------------------------------------------------------------------------- /app/conf/systemconfig.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from app.helper import DictHelper 4 | from app.utils.commons import singleton 5 | 6 | 7 | @singleton 8 | class SystemConfig: 9 | 10 | # 系统设置 11 | systemconfig = { 12 | # 默认下载设置 13 | "DefaultDownloadSetting": None, 14 | # CookieCloud的设置 15 | "CookieCloud": {}, 16 | # 自动获取Cookie的用户信息 17 | "CookieUserInfo": {}, 18 | # 用户自定义CSS/JavsScript 19 | "CustomScript": {}, 20 | # 播放限速设置 21 | "SpeedLimit": {} 22 | } 23 | 24 | def __init__(self): 25 | self.init_config() 26 | 27 | def init_config(self, key=None): 28 | """ 29 | 缓存系统设置 30 | """ 31 | def __set_value(_key, _value): 32 | if isinstance(_value, dict) \ 33 | or isinstance(_value, list): 34 | dict_value = DictHelper().get("SystemConfig", _key) 35 | if dict_value: 36 | self.systemconfig[_key] = json.loads(dict_value) 37 | else: 38 | self.systemconfig[_key] = {} 39 | else: 40 | self.systemconfig[_key] = DictHelper().get("SystemConfig", _key) 41 | 42 | if key: 43 | __set_value(key, self.systemconfig.get(key)) 44 | else: 45 | for key, value in self.systemconfig.items(): 46 | __set_value(key, value) 47 | 48 | def set_system_config(self, key, value): 49 | """ 50 | 设置系统设置 51 | """ 52 | if isinstance(value, dict) \ 53 | or isinstance(value, list): 54 | if value: 55 | value = json.dumps(value) 56 | else: 57 | value = None 58 | DictHelper().set("SystemConfig", key, value) 59 | self.init_config(key) 60 | 61 | def get_system_config(self, key=None): 62 | """ 63 | 获取系统设置 64 | """ 65 | if not key: 66 | return self.systemconfig 67 | return self.systemconfig.get(key) 68 | -------------------------------------------------------------------------------- /app/db/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import log 3 | from config import Config 4 | from .main_db import MainDb 5 | from .main_db import DbPersist 6 | from .media_db import MediaDb 7 | from alembic.config import Config as AlembicConfig 8 | from alembic.command import upgrade as alembic_upgrade 9 | 10 | 11 | def init_db(): 12 | """ 13 | 初始化数据库 14 | """ 15 | log.console('开始初始化数据库...') 16 | MediaDb().init_db() 17 | MainDb().init_db() 18 | log.console('数据库初始化完成') 19 | 20 | 21 | def init_data(): 22 | """ 23 | 初始化数据 24 | """ 25 | log.console('开始初始化数据...') 26 | MainDb().init_data() 27 | log.console('数据初始化完成') 28 | 29 | 30 | def update_db(): 31 | """ 32 | 更新数据库 33 | """ 34 | db_location = os.path.join(Config().get_config_path(), 'user.db') 35 | script_location = os.path.join(Config().get_root_path(), 'db_scripts') 36 | log.console('开始更新数据库...') 37 | try: 38 | alembic_cfg = AlembicConfig() 39 | alembic_cfg.set_main_option('script_location', script_location) 40 | alembic_cfg.set_main_option('sqlalchemy.url', f"sqlite:///{db_location}") 41 | alembic_upgrade(alembic_cfg, 'head') 42 | except Exception as e: 43 | print(str(e)) 44 | log.console('数据库更新完成') 45 | -------------------------------------------------------------------------------- /app/db/main_db.py: -------------------------------------------------------------------------------- 1 | import os 2 | import threading 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.orm import sessionmaker, scoped_session 5 | from sqlalchemy.pool import QueuePool 6 | 7 | from app.db.models import Base 8 | from app.utils import ExceptionUtils, PathUtils 9 | from config import Config 10 | 11 | lock = threading.Lock() 12 | _Engine = create_engine( 13 | f"sqlite:///{os.path.join(Config().get_config_path(), 'user.db')}?check_same_thread=False", 14 | echo=False, 15 | poolclass=QueuePool, 16 | pool_pre_ping=True, 17 | pool_size=50, 18 | pool_recycle=60 * 10, 19 | max_overflow=0 20 | ) 21 | _Session = scoped_session(sessionmaker(bind=_Engine, 22 | autoflush=True, 23 | autocommit=False, 24 | expire_on_commit=False)) 25 | 26 | 27 | class MainDb: 28 | 29 | @property 30 | def session(self): 31 | return _Session() 32 | 33 | @staticmethod 34 | def init_db(): 35 | with lock: 36 | Base.metadata.create_all(_Engine) 37 | 38 | def init_data(self): 39 | """ 40 | 读取config目录下的sql文件,并初始化到数据库,只处理一次 41 | """ 42 | config = Config().get_config() 43 | init_files = Config().get_config("app").get("init_files") or [] 44 | config_dir = os.path.join(Config().get_root_path(), "config") 45 | sql_files = PathUtils.get_dir_level1_files(in_path=config_dir, exts=".sql") 46 | config_flag = False 47 | for sql_file in sql_files: 48 | if os.path.basename(sql_file) not in init_files: 49 | config_flag = True 50 | with open(sql_file, "r", encoding="utf-8") as f: 51 | sql_list = f.read().split(';\n') 52 | for sql in sql_list: 53 | try: 54 | self.excute(sql) 55 | self.commit() 56 | except Exception as err: 57 | print(str(err)) 58 | init_files.append(os.path.basename(sql_file)) 59 | if config_flag: 60 | config['app']['init_files'] = init_files 61 | Config().save_config(config) 62 | 63 | def insert(self, data): 64 | """ 65 | 插入数据 66 | """ 67 | if isinstance(data, list): 68 | self.session.add_all(data) 69 | else: 70 | self.session.add(data) 71 | 72 | def query(self, *obj): 73 | """ 74 | 查询对象 75 | """ 76 | return self.session.query(*obj) 77 | 78 | def excute(self, sql): 79 | """ 80 | 执行SQL语句 81 | """ 82 | self.session.execute(sql) 83 | 84 | def flush(self): 85 | """ 86 | 刷写 87 | """ 88 | self.session.flush() 89 | 90 | def commit(self): 91 | """ 92 | 提交事务 93 | """ 94 | self.session.commit() 95 | 96 | def rollback(self): 97 | """ 98 | 回滚事务 99 | """ 100 | self.session.rollback() 101 | 102 | 103 | class DbPersist(object): 104 | """ 105 | 数据库持久化装饰器 106 | """ 107 | 108 | def __init__(self, db): 109 | self.db = db 110 | 111 | def __call__(self, f): 112 | def persist(*args, **kwargs): 113 | try: 114 | ret = f(*args, **kwargs) 115 | self.db.commit() 116 | return True if ret is None else ret 117 | except Exception as e: 118 | ExceptionUtils.exception_traceback(e) 119 | self.db.rollback() 120 | return False 121 | 122 | return persist 123 | -------------------------------------------------------------------------------- /app/downloader/__init__.py: -------------------------------------------------------------------------------- 1 | from .downloader import Downloader 2 | -------------------------------------------------------------------------------- /app/downloader/client/__init__.py: -------------------------------------------------------------------------------- 1 | from .qbittorrent import Qbittorrent 2 | from .transmission import Transmission 3 | -------------------------------------------------------------------------------- /app/downloader/client/_base.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from abc import ABCMeta, abstractmethod 3 | 4 | from config import Config 5 | 6 | 7 | class _IDownloadClient(metaclass=ABCMeta): 8 | 9 | @abstractmethod 10 | def match(self, ctype): 11 | """ 12 | 匹配实例 13 | """ 14 | pass 15 | 16 | @abstractmethod 17 | def connect(self): 18 | """ 19 | 连接 20 | """ 21 | pass 22 | 23 | @abstractmethod 24 | def get_status(self): 25 | """ 26 | 检查连通性 27 | """ 28 | pass 29 | 30 | @abstractmethod 31 | def get_torrents(self, ids, status, tag): 32 | """ 33 | 按条件读取种子信息 34 | :param ids: 种子ID,单个ID或者ID列表 35 | :param status: 种子状态过滤 36 | :param tag: 种子标签过滤 37 | :return: 种子信息列表 38 | """ 39 | pass 40 | 41 | @abstractmethod 42 | def get_downloading_torrents(self, tag): 43 | """ 44 | 读取下载中的种子信息 45 | """ 46 | pass 47 | 48 | @abstractmethod 49 | def get_completed_torrents(self, tag): 50 | """ 51 | 读取下载完成的种子信息 52 | """ 53 | pass 54 | 55 | @abstractmethod 56 | def set_torrents_status(self, ids, tags=None): 57 | """ 58 | 迁移完成后设置种子标签为 已整理 59 | :param ids: 种子ID列表 60 | :param tags: 种子标签列表 61 | """ 62 | pass 63 | 64 | @abstractmethod 65 | def get_transfer_task(self, tag): 66 | """ 67 | 获取需要转移的种子列表 68 | """ 69 | pass 70 | 71 | @abstractmethod 72 | def get_remove_torrents(self, config): 73 | """ 74 | 获取需要清理的种子清单 75 | :param config: 删种策略 76 | :return: 种子ID列表 77 | """ 78 | pass 79 | 80 | @abstractmethod 81 | def add_torrent(self, **kwargs): 82 | """ 83 | 添加下载任务 84 | """ 85 | pass 86 | 87 | @abstractmethod 88 | def start_torrents(self, ids): 89 | """ 90 | 下载控制:开始 91 | """ 92 | pass 93 | 94 | @abstractmethod 95 | def stop_torrents(self, ids): 96 | """ 97 | 下载控制:停止 98 | """ 99 | pass 100 | 101 | @abstractmethod 102 | def delete_torrents(self, delete_file, ids): 103 | """ 104 | 删除种子 105 | """ 106 | pass 107 | 108 | @abstractmethod 109 | def get_download_dirs(self): 110 | """ 111 | 获取下载目录清单 112 | """ 113 | pass 114 | 115 | @staticmethod 116 | def get_replace_path(path): 117 | """ 118 | 对目录路径进行转换 119 | """ 120 | if not path: 121 | return "" 122 | downloaddir = Config().get_config('downloaddir') or [] 123 | path = os.path.normpath(path) 124 | for attr in downloaddir: 125 | if not attr.get("save_path") or not attr.get("container_path"): 126 | continue 127 | save_path = os.path.normpath(attr.get("save_path")) 128 | container_path = os.path.normpath(attr.get("container_path")) 129 | if path.startswith(save_path): 130 | return path.replace(save_path, container_path) 131 | return path 132 | 133 | @abstractmethod 134 | def change_torrent(self, **kwargs): 135 | """ 136 | 修改种子状态 137 | """ 138 | pass 139 | 140 | @abstractmethod 141 | def get_downloading_progress(self): 142 | """ 143 | 获取下载进度 144 | """ 145 | pass 146 | 147 | @abstractmethod 148 | def set_speed_limit(self, **kwargs): 149 | """ 150 | 设置速度限制 151 | """ 152 | pass 153 | -------------------------------------------------------------------------------- /app/helper/__init__.py: -------------------------------------------------------------------------------- 1 | from .chrome_helper import ChromeHelper 2 | from .indexer_helper import IndexerHelper, IndexerConf 3 | from .meta_helper import MetaHelper 4 | from .progress_helper import ProgressHelper 5 | from .security_helper import SecurityHelper 6 | from .thread_helper import ThreadHelper 7 | from .db_helper import DbHelper 8 | from .dict_helper import DictHelper 9 | from .display_helper import DisplayHelper 10 | from .site_helper import SiteHelper 11 | from .ocr_helper import OcrHelper 12 | from .opensubtitles import OpenSubtitles 13 | from .words_helper import WordsHelper 14 | from .submodule_helper import SubmoduleHelper 15 | from .cookiecloud_helper import CookieCloudHelper 16 | from .ffmpeg_helper import FfmpegHelper 17 | -------------------------------------------------------------------------------- /app/helper/cookiecloud_helper.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from app.utils import RequestUtils, StringUtils 4 | 5 | 6 | class CookieCloudHelper(object): 7 | _req = None 8 | _server = None 9 | _key = None 10 | _password = None 11 | 12 | def __init__(self, server, key, password): 13 | self._server = server 14 | if self._server: 15 | if not self._server.startswith("http"): 16 | self._server = "http://%s" % self._server 17 | if self._server.endswith("/"): 18 | self._server = self._server[:-1] 19 | self._key = key 20 | self._password = password 21 | self._req = RequestUtils(content_type="application/json") 22 | 23 | def download_data(self): 24 | """ 25 | 从CookieCloud下载数据 26 | """ 27 | if not self._server or not self._key or not self._password: 28 | return {}, "CookieCloud参数不正确" 29 | req_url = "%s/get/%s" % (self._server, self._key) 30 | ret = self._req.post_res(url=req_url, json={"password": self._password}) 31 | if ret and ret.status_code == 200: 32 | result = ret.json() 33 | if not result: 34 | return {}, "" 35 | if result.get("cookie_data"): 36 | return result.get("cookie_data"), "" 37 | return result, "" 38 | elif ret: 39 | return {}, "同步CookieCloud失败,错误码:%s" % ret.status_code 40 | else: 41 | return {}, "CookieCloud请求失败,请检查服务器地址、用户KEY及加密密码是否正确" 42 | -------------------------------------------------------------------------------- /app/helper/dict_helper.py: -------------------------------------------------------------------------------- 1 | from app.db import MainDb, DbPersist 2 | from app.db.models import SYSTEMDICT 3 | 4 | 5 | class DictHelper: 6 | 7 | _db = MainDb() 8 | 9 | @DbPersist(_db) 10 | def set(self, dtype, key, value, note=""): 11 | """ 12 | 设置字典值 13 | :param dtype: 字典类型 14 | :param key: 字典Key 15 | :param value: 字典值 16 | :param note: 备注 17 | :return: True False 18 | """ 19 | if not dtype or not key or not value: 20 | return False 21 | if self.exists(dtype, key): 22 | return self._db.query(SYSTEMDICT).filter(SYSTEMDICT.TYPE == dtype, 23 | SYSTEMDICT.KEY == key).update( 24 | { 25 | "VALUE": value 26 | } 27 | ) 28 | else: 29 | return self._db.insert(SYSTEMDICT( 30 | TYPE=dtype, 31 | KEY=key, 32 | VALUE=value, 33 | NOTE=note 34 | )) 35 | 36 | def get(self, dtype, key): 37 | """ 38 | 查询字典值 39 | :param dtype: 字典类型 40 | :param key: 字典Key 41 | :return: 返回字典值 42 | """ 43 | if not dtype or not key: 44 | return "" 45 | ret = self._db.query(SYSTEMDICT.VALUE).filter(SYSTEMDICT.TYPE == dtype, 46 | SYSTEMDICT.KEY == key).first() 47 | if ret: 48 | return ret[0] 49 | else: 50 | return "" 51 | 52 | @DbPersist(_db) 53 | def delete(self, dtype, key): 54 | """ 55 | 删除字典值 56 | :param dtype: 字典类型 57 | :param key: 字典Key 58 | :return: True False 59 | """ 60 | if not dtype or not key: 61 | return False 62 | return self._db.query(SYSTEMDICT).filter(SYSTEMDICT.TYPE == dtype, 63 | SYSTEMDICT.KEY == key).delete() 64 | 65 | def exists(self, dtype, key): 66 | """ 67 | 查询字典是否存在 68 | :param dtype: 字典类型 69 | :param key: 字典Key 70 | :return: True False 71 | """ 72 | if not dtype or not key: 73 | return False 74 | ret = self._db.query(SYSTEMDICT).filter(SYSTEMDICT.TYPE == dtype, 75 | SYSTEMDICT.KEY == key).count() 76 | if ret > 0: 77 | return True 78 | else: 79 | return False 80 | -------------------------------------------------------------------------------- /app/helper/display_helper.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pyvirtualdisplay import Display 4 | 5 | from app.utils.commons import singleton 6 | from app.utils import ExceptionUtils 7 | from config import XVFB_PATH 8 | 9 | 10 | @singleton 11 | class DisplayHelper(object): 12 | _display = None 13 | 14 | def __init__(self): 15 | self.init_config() 16 | 17 | def init_config(self): 18 | self.quit() 19 | if self.can_display(): 20 | try: 21 | self._display = Display(visible=False, size=(1024, 768)) 22 | self._display.start() 23 | os.environ["NASTOOL_DISPLAY"] = "true" 24 | except Exception as err: 25 | ExceptionUtils.exception_traceback(err) 26 | 27 | def get_display(self): 28 | return self._display 29 | 30 | def quit(self): 31 | os.environ["NASTOOL_DISPLAY"] = "" 32 | if self._display: 33 | self._display.stop() 34 | 35 | @staticmethod 36 | def can_display(): 37 | for path in XVFB_PATH: 38 | if os.path.exists(path): 39 | return True 40 | return False 41 | 42 | def __del__(self): 43 | self.quit() 44 | -------------------------------------------------------------------------------- /app/helper/ffmpeg_helper.py: -------------------------------------------------------------------------------- 1 | from app.utils import SystemUtils 2 | 3 | 4 | class FfmpegHelper: 5 | 6 | @staticmethod 7 | def get_thumb_image_from_video(video_path, image_path, frames="00:03:01"): 8 | """ 9 | 使用ffmpeg从视频文件中截取缩略图 10 | """ 11 | if not video_path or not image_path: 12 | return False 13 | cmd = 'ffmpeg -i "{video_path}" -ss {frames} -vframes 1 -f image2 "{image_path}"'.format(video_path=video_path, 14 | frames=frames, 15 | image_path=image_path) 16 | result = SystemUtils.execute(cmd) 17 | if result: 18 | return True 19 | return False 20 | -------------------------------------------------------------------------------- /app/helper/ocr_helper.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | from app.utils import RequestUtils 4 | from config import DEFAULT_OCR_SERVER 5 | 6 | 7 | class OcrHelper: 8 | req = None 9 | _ocr_b64_url = "%s/captcha/base64" % DEFAULT_OCR_SERVER 10 | 11 | def __init__(self): 12 | self.req = RequestUtils(content_type="application/json") 13 | 14 | def get_captcha_text(self, image_url=None, image_b64=None): 15 | """ 16 | 根据图片地址,获取验证码图片,并识别内容 17 | """ 18 | if not image_url and not image_b64: 19 | return "" 20 | if image_url: 21 | ret = self.req.get_res(image_url) 22 | if ret is not None: 23 | image_bin = ret.content 24 | if not image_bin: 25 | return "" 26 | image_b64 = base64.b64encode(image_bin).decode() 27 | ret = self.req.post_res(url=self._ocr_b64_url, 28 | json={"base64_img": image_b64}) 29 | if ret: 30 | return ret.json().get("result") 31 | return "" 32 | -------------------------------------------------------------------------------- /app/helper/opensubtitles.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | from urllib.parse import quote 3 | 4 | from pyquery import PyQuery 5 | 6 | import log 7 | from app.helper.chrome_helper import ChromeHelper 8 | from config import Config 9 | 10 | 11 | class OpenSubtitles: 12 | _cookie = "" 13 | _ua = None 14 | _url_imdbid = "https://www.opensubtitles.org/zh/search/imdbid-%s/sublanguageid-chi" 15 | _url_keyword = "https://www.opensubtitles.org/zh/search/moviename-%s/sublanguageid-chi" 16 | 17 | def __init__(self): 18 | self._ua = Config().get_ua() 19 | 20 | def search_subtitles(self, query): 21 | if query.get("imdbid"): 22 | return self.__search_subtitles_by_imdbid(query.get("imdbid")) 23 | else: 24 | return self.__search_subtitles_by_keyword("%s %s" % (query.get("name"), query.get("year"))) 25 | 26 | def __search_subtitles_by_imdbid(self, imdbid): 27 | """ 28 | 按TMDBID搜索OpenSubtitles 29 | """ 30 | return self.__parse_opensubtitles_results(url=self._url_imdbid % str(imdbid).replace("tt", "")) 31 | 32 | def __search_subtitles_by_keyword(self, keyword): 33 | """ 34 | 按关键字搜索OpenSubtitles 35 | """ 36 | return self.__parse_opensubtitles_results(url=self._url_keyword % quote(keyword)) 37 | 38 | @classmethod 39 | @lru_cache(maxsize=128) 40 | def __parse_opensubtitles_results(cls, url): 41 | """ 42 | 搜索并解析结果 43 | """ 44 | chrome = ChromeHelper() 45 | if not chrome.get_status(): 46 | log.error("【Subtitle】未找到浏览器内核,当前环境无法检索opensubtitles字幕!") 47 | return [] 48 | # 访问页面 49 | if not chrome.visit(url): 50 | log.error("【Subtitle】无法连接opensubtitles.org!") 51 | return [] 52 | # 源码 53 | html_text = chrome.get_html() 54 | # Cookie 55 | cls._cookie = chrome.get_cookies() 56 | # 解析列表 57 | ret_subtitles = [] 58 | html_doc = PyQuery(html_text) 59 | global_season = '' 60 | for tr in html_doc('#search_results > tbody > tr:not([style])'): 61 | tr_doc = PyQuery(tr) 62 | # 季 63 | season = tr_doc('span[id^="season-"] > a > b').text() 64 | if season: 65 | global_season = season 66 | continue 67 | # 集 68 | episode = tr_doc('span[itemprop="episodeNumber"]').text() 69 | # 标题 70 | title = tr_doc('strong > a.bnone').text() 71 | # 描述 下载链接 72 | if not global_season: 73 | description = tr_doc('td:nth-child(1)').text() 74 | if description and len(description.split("\n")) > 1: 75 | description = description.split("\n")[1] 76 | link = tr_doc('td:nth-child(5) > a').attr("href") 77 | else: 78 | description = tr_doc('span[itemprop="name"]').text() 79 | link = tr_doc('a[href^="/download/"]').attr("href") 80 | if link: 81 | link = "https://www.opensubtitles.org%s" % link 82 | else: 83 | continue 84 | ret_subtitles.append({ 85 | "season": global_season, 86 | "episode": episode, 87 | "title": title, 88 | "description": description, 89 | "link": link 90 | }) 91 | return ret_subtitles 92 | 93 | def get_cookie(self): 94 | """ 95 | 返回Cookie 96 | """ 97 | return self._cookie 98 | 99 | def get_ua(self): 100 | """ 101 | 返回User-Agent 102 | """ 103 | return self._ua 104 | -------------------------------------------------------------------------------- /app/helper/progress_helper.py: -------------------------------------------------------------------------------- 1 | from app.utils.commons import singleton 2 | 3 | 4 | @singleton 5 | class ProgressHelper(object): 6 | _process_detail = {} 7 | 8 | def __init__(self): 9 | self._process_detail = {} 10 | 11 | def init_config(self): 12 | pass 13 | 14 | def reset(self, ptype="search"): 15 | self._process_detail[ptype] = { 16 | "enable": False, 17 | "value": 0, 18 | "text": "请稍候..." 19 | } 20 | 21 | def start(self, ptype="search"): 22 | self.reset(ptype) 23 | self._process_detail[ptype]['enable'] = True 24 | 25 | def end(self, ptype="search"): 26 | if not self._process_detail.get(ptype): 27 | return 28 | self._process_detail[ptype]['enable'] = False 29 | 30 | def update(self, value=None, text=None, ptype="search"): 31 | if not self._process_detail.get(ptype, {}).get('enable'): 32 | return 33 | if value: 34 | self._process_detail[ptype]['value'] = value 35 | if text: 36 | self._process_detail[ptype]['text'] = text 37 | 38 | def get_process(self, ptype="search"): 39 | return self._process_detail.get(ptype) 40 | -------------------------------------------------------------------------------- /app/helper/security_helper.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | 3 | from app.utils import ExceptionUtils 4 | from config import Config 5 | 6 | 7 | class SecurityHelper: 8 | media_server_webhook_allow_ip = {} 9 | telegram_webhook_allow_ip = {} 10 | synology_webhook_allow_ip = {} 11 | 12 | def __init__(self): 13 | security = Config().get_config('security') 14 | if security: 15 | self.media_server_webhook_allow_ip = security.get('media_server_webhook_allow_ip') or {} 16 | self.telegram_webhook_allow_ip = security.get('telegram_webhook_allow_ip') or {} 17 | self.synology_webhook_allow_ip = security.get('synology_webhook_allow_ip') or {} 18 | 19 | def check_mediaserver_ip(self, ip): 20 | return self.allow_access(self.media_server_webhook_allow_ip, ip) 21 | 22 | def check_telegram_ip(self, ip): 23 | return self.allow_access(self.telegram_webhook_allow_ip, ip) 24 | 25 | def check_synology_ip(self, ip): 26 | return self.allow_access(self.synology_webhook_allow_ip, ip) 27 | 28 | def check_slack_ip(self, ip): 29 | return self.allow_access({"ipve": "127.0.0.1"}, ip) 30 | 31 | @staticmethod 32 | def allow_access(allow_ips, ip): 33 | """ 34 | 判断IP是否合法 35 | :param allow_ips: 充许的IP范围 {"ipv4":, "ipv6":} 36 | :param ip: 需要检查的ip 37 | """ 38 | if not allow_ips: 39 | return True 40 | try: 41 | ipaddr = ipaddress.ip_address(ip) 42 | if ipaddr.version == 4: 43 | if not allow_ips.get('ipv4'): 44 | return True 45 | allow_ipv4s = allow_ips.get('ipv4').split(",") 46 | for allow_ipv4 in allow_ipv4s: 47 | if ipaddr in ipaddress.ip_network(allow_ipv4): 48 | return True 49 | elif ipaddr.ipv4_mapped: 50 | if not allow_ips.get('ipv4'): 51 | return True 52 | allow_ipv4s = allow_ips.get('ipv4').split(",") 53 | for allow_ipv4 in allow_ipv4s: 54 | if ipaddr.ipv4_mapped in ipaddress.ip_network(allow_ipv4): 55 | return True 56 | else: 57 | if not allow_ips.get('ipv6'): 58 | return True 59 | allow_ipv6s = allow_ips.get('ipv6').split(",") 60 | for allow_ipv6 in allow_ipv6s: 61 | if ipaddr in ipaddress.ip_network(allow_ipv6): 62 | return True 63 | except Exception as e: 64 | ExceptionUtils.exception_traceback(e) 65 | return False 66 | -------------------------------------------------------------------------------- /app/helper/site_helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from lxml import etree 3 | 4 | 5 | class SiteHelper: 6 | @classmethod 7 | def is_logged_in(cls, html_text): 8 | """ 9 | 判断站点是否已经登陆 10 | :param html_text: 11 | :return: 12 | """ 13 | html = etree.HTML(html_text) 14 | if not html: 15 | return False 16 | # 存在明显的密码输入框,说明未登录 17 | if html.xpath("//input[@type='password']"): 18 | return False 19 | # 是否存在登出和用户面板等链接 20 | logout_or_usercp = html.xpath('//a[contains(@href, "logout") or contains(@data-url, "logout")' 21 | ' or contains(@href, "mybonus") ' 22 | ' or contains(@onclick, "logout") or contains(@href, "usercp")]') 23 | 24 | if logout_or_usercp: 25 | return True 26 | 27 | user_info_div = html.xpath('//div[@class="user-info-side"]') 28 | if user_info_div: 29 | return True 30 | 31 | return False 32 | -------------------------------------------------------------------------------- /app/helper/submodule_helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import importlib 3 | import pkgutil 4 | 5 | 6 | class SubmoduleHelper: 7 | @classmethod 8 | def import_submodules(cls, package, filter_func=lambda name, obj: True): 9 | """ 10 | 导入子模块 11 | :param package: 父包名 12 | :param filter_func: 子模块过滤函数,入参为模块名和模块对象,返回True则导入,否则不导入 13 | :return: 14 | """ 15 | 16 | submodules = [] 17 | packages = importlib.import_module(package).__path__ 18 | for importer, package_name, _ in pkgutil.iter_modules(packages): 19 | full_package_name = f'{package}.{package_name}' 20 | if full_package_name.startswith('_'): 21 | continue 22 | module = importlib.import_module(full_package_name) 23 | for name, obj in module.__dict__.items(): 24 | if name.startswith('_'): 25 | continue 26 | if isinstance(obj, type) and filter_func(name, obj): 27 | submodules.append(obj) 28 | 29 | return submodules 30 | -------------------------------------------------------------------------------- /app/helper/thread_helper.py: -------------------------------------------------------------------------------- 1 | from concurrent.futures import ThreadPoolExecutor 2 | 3 | from app.utils.commons import singleton 4 | 5 | 6 | @singleton 7 | class ThreadHelper: 8 | _thread_num = 50 9 | executor = None 10 | 11 | def __init__(self): 12 | self.executor = ThreadPoolExecutor(max_workers=self._thread_num) 13 | 14 | def init_config(self): 15 | pass 16 | 17 | def start_thread(self, func, kwargs): 18 | self.executor.submit(func, *kwargs) 19 | -------------------------------------------------------------------------------- /app/indexer/__init__.py: -------------------------------------------------------------------------------- 1 | from .indexer import Indexer 2 | -------------------------------------------------------------------------------- /app/indexer/client/__init__.py: -------------------------------------------------------------------------------- 1 | from .builtin import BuiltinIndexer 2 | -------------------------------------------------------------------------------- /app/indexer/client/_rarbg.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | import log 4 | from app.utils import RequestUtils 5 | from config import Config 6 | 7 | 8 | class Rarbg: 9 | _appid = "nastool" 10 | _req = None 11 | _token = None 12 | _api_url = "http://torrentapi.org/pubapi_v2.php" 13 | 14 | def __init__(self): 15 | self.init_config() 16 | 17 | def init_config(self): 18 | session = requests.session() 19 | self._req = RequestUtils(proxies=Config().get_proxies(), session=session, timeout=10) 20 | self.__get_token() 21 | 22 | def __get_token(self): 23 | if self._token: 24 | return 25 | res = self._req.get_res(url=self._api_url, params={'app_id': self._appid, 'get_token': 'get_token'}) 26 | if res and res.json(): 27 | self._token = res.json().get('token') 28 | 29 | def search(self, keyword, indexer, imdb_id=None): 30 | if not keyword: 31 | return [] 32 | self.__get_token() 33 | if not self._token: 34 | log.warn(f"【INDEXER】{indexer.name} 未获取到token,无法搜索") 35 | return [] 36 | params = {'app_id': self._appid, 'mode': 'search', 'token': self._token, 'format': 'json_extended', 'limit': 100} 37 | if imdb_id: 38 | params['search_imdb'] = imdb_id 39 | else: 40 | params['search_string'] = keyword 41 | res = self._req.get_res(url=self._api_url, params=params) 42 | torrents = [] 43 | if res and res.status_code == 200: 44 | results = res.json().get('torrent_results') or [] 45 | for result in results: 46 | if not result or not result.get('title'): 47 | continue 48 | torrent = {'indexer': indexer.id, 49 | 'title': result.get('title'), 50 | 'enclosure': result.get('download'), 51 | 'size': result.get('size'), 52 | 'seeders': result.get('seeders'), 53 | 'peers': result.get('leechers'), 54 | 'freeleech': True, 55 | 'downloadvolumefactor': 0.0, 56 | 'uploadvolumefactor': 1.0, 57 | 'page_url': result.get('info_page'), 58 | 'imdbid': result.get('episode_info').get('imdb') if result.get('episode_info') else ''} 59 | torrents.append(torrent) 60 | elif res is not None: 61 | log.warn(f"【INDEXER】{indexer.name} 搜索失败,错误码:{res.status_code}") 62 | return [] 63 | else: 64 | log.warn(f"【INDEXER】{indexer.name} 搜索失败,无法连接 torrentapi.org") 65 | return [] 66 | return torrents 67 | -------------------------------------------------------------------------------- /app/indexer/client/jackett.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from app.utils import ExceptionUtils 4 | from app.utils.types import IndexerType 5 | from config import Config 6 | from app.indexer.client._base import _IIndexClient 7 | from app.utils import RequestUtils 8 | from app.helper import IndexerConf 9 | 10 | 11 | class Jackett(_IIndexClient): 12 | schema = "jackett" 13 | _client_config = {} 14 | index_type = IndexerType.JACKETT.value 15 | _password = None 16 | 17 | def __init__(self, config=None): 18 | super().__init__() 19 | if config: 20 | self._client_config = config 21 | else: 22 | self._client_config = Config().get_config('jackett') 23 | self.init_config() 24 | 25 | def init_config(self): 26 | if self._client_config: 27 | self.api_key = self._client_config.get('api_key') 28 | self._password = self._client_config.get('password') 29 | self.host = self._client_config.get('host') 30 | if self.host: 31 | if not self.host.startswith('http'): 32 | self.host = "http://" + self.host 33 | if not self.host.endswith('/'): 34 | self.host = self.host + "/" 35 | 36 | def get_status(self): 37 | """ 38 | 检查连通性 39 | :return: True、False 40 | """ 41 | if not self.api_key or not self.host: 42 | return False 43 | return True if self.get_indexers() else False 44 | 45 | @classmethod 46 | def match(cls, ctype): 47 | return True if ctype in [cls.schema, cls.index_type] else False 48 | 49 | def get_indexers(self): 50 | """ 51 | 获取配置的jackett indexer 52 | :return: indexer 信息 [(indexerId, indexerName, url)] 53 | """ 54 | # 获取Cookie 55 | cookie = None 56 | session = requests.session() 57 | res = RequestUtils(session=session).post_res(url=f"{self.host}UI/Dashboard", 58 | params={"password": self._password}) 59 | if res and session.cookies: 60 | cookie = session.cookies.get_dict() 61 | indexer_query_url = f"{self.host}api/v2.0/indexers?configured=true" 62 | try: 63 | ret = RequestUtils(cookies=cookie).get_res(indexer_query_url) 64 | if not ret or not ret.json(): 65 | return [] 66 | return [IndexerConf({"id": v["id"], 67 | "name": v["name"], 68 | "domain": f'{self.host}api/v2.0/indexers/{v["id"]}/results/torznab/', 69 | "public": True if v['type'] == 'public' else False, 70 | "builtin": False}) 71 | for v in ret.json()] 72 | except Exception as e2: 73 | ExceptionUtils.exception_traceback(e2) 74 | return [] 75 | 76 | def search(self, *kwargs): 77 | return super().search(*kwargs) 78 | -------------------------------------------------------------------------------- /app/indexer/client/prowlarr.py: -------------------------------------------------------------------------------- 1 | from app.utils import ExceptionUtils 2 | from app.utils.types import IndexerType 3 | from config import Config 4 | from app.indexer.client._base import _IIndexClient 5 | from app.utils import RequestUtils 6 | from app.helper import IndexerConf 7 | 8 | 9 | class Prowlarr(_IIndexClient): 10 | schema = "prowlarr" 11 | _client_config = {} 12 | index_type = IndexerType.PROWLARR.value 13 | 14 | def __init__(self, config=None): 15 | super().__init__() 16 | if config: 17 | self._client_config = config 18 | else: 19 | self._client_config = Config().get_config('prowlarr') 20 | self.init_config() 21 | 22 | def init_config(self): 23 | if self._client_config: 24 | self.api_key = self._client_config.get('api_key') 25 | self.host = self._client_config.get('host') 26 | if self.host: 27 | if not self.host.startswith('http'): 28 | self.host = "http://" + self.host 29 | if not self.host.endswith('/'): 30 | self.host = self.host + "/" 31 | 32 | @classmethod 33 | def match(cls, ctype): 34 | return True if ctype in [cls.schema, cls.index_type] else False 35 | 36 | def get_status(self): 37 | """ 38 | 检查连通性 39 | :return: True、False 40 | """ 41 | if not self.api_key or not self.host: 42 | return False 43 | return True if self.get_indexers() else False 44 | 45 | def get_indexers(self): 46 | """ 47 | 获取配置的prowlarr indexer 48 | :return: indexer 信息 [(indexerId, indexerName, url)] 49 | """ 50 | indexer_query_url = f"{self.host}api/v1/indexerstats?apikey={self.api_key}" 51 | try: 52 | ret = RequestUtils().get_res(indexer_query_url) 53 | except Exception as e2: 54 | ExceptionUtils.exception_traceback(e2) 55 | return [] 56 | if not ret: 57 | return [] 58 | indexers = ret.json().get("indexers", []) 59 | return [IndexerConf({"id": v["indexerId"], 60 | "name": v["indexerName"], 61 | "domain": f'{self.host}{v["indexerId"]}/api', 62 | "builtin": False}) 63 | for v in indexers] 64 | 65 | def search(self, *kwargs): 66 | return super().search(*kwargs) 67 | -------------------------------------------------------------------------------- /app/media/__init__.py: -------------------------------------------------------------------------------- 1 | from .category import Category 2 | from .media import Media 3 | from .scraper import Scraper 4 | from .douban import DouBan 5 | from .bangumi import Bangumi 6 | -------------------------------------------------------------------------------- /app/media/bangumi.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from functools import lru_cache 3 | 4 | import requests 5 | 6 | from app.utils import RequestUtils 7 | from app.utils.types import MediaType 8 | 9 | 10 | class Bangumi(object): 11 | """ 12 | https://bangumi.github.io/api/ 13 | """ 14 | 15 | _urls = { 16 | "calendar": "calendar", 17 | "detail": "v0/subjects/%s", 18 | } 19 | _base_url = "https://api.bgm.tv/" 20 | _req = RequestUtils(session=requests.Session()) 21 | _page_num = 30 22 | 23 | def __init__(self): 24 | pass 25 | 26 | @classmethod 27 | @lru_cache(maxsize=128) 28 | def __invoke(cls, url, **kwargs): 29 | req_url = cls._base_url + url 30 | params = {} 31 | if kwargs: 32 | params.update(kwargs) 33 | resp = cls._req.get_res(url=req_url, params=params) 34 | return resp.json() if resp else None 35 | 36 | def calendar(self): 37 | """ 38 | 获取每日放送 39 | """ 40 | return self.__invoke(self._urls["calendar"], _ts=datetime.strftime(datetime.now(), '%Y%m%d')) 41 | 42 | def detail(self, bid): 43 | """ 44 | 获取番剧详情 45 | """ 46 | return self.__invoke(self._urls["detail"] % bid, _ts=datetime.strftime(datetime.now(), '%Y%m%d')) 47 | 48 | @staticmethod 49 | def __dict_item(item, weekday): 50 | """ 51 | 转换为字典 52 | """ 53 | bid = item.get("id") 54 | detail = item.get("url") 55 | title = item.get("name_cn", item.get("name")) 56 | air_date = item.get("air_date") 57 | rating = item.get("rating") 58 | if rating: 59 | score = rating.get("score") 60 | else: 61 | score = 0 62 | images = item.get("images") 63 | if images: 64 | image = images.get("large") 65 | else: 66 | image = '' 67 | summary = item.get("summary") 68 | return { 69 | 'id': "BG:%s" % bid, 70 | 'orgid': bid, 71 | 'title': title, 72 | 'year': air_date[:4] if air_date else "", 73 | 'type': 'TV', 74 | 'media_type': MediaType.TV.value, 75 | 'vote': score, 76 | 'image': image, 77 | 'overview': summary, 78 | 'url': detail, 79 | 'weekday': weekday 80 | } 81 | 82 | def get_bangumi_calendar(self, page=1, week=None): 83 | """ 84 | 获取每日放送 85 | """ 86 | infos = self.calendar() 87 | if not infos: 88 | return [] 89 | start_pos = (int(page) - 1) * self._page_num 90 | ret_list = [] 91 | pos = 0 92 | for info in infos: 93 | weeknum = info.get("weekday", {}).get("id") 94 | if week and int(weeknum) != int(week): 95 | continue 96 | weekday = info.get("weekday", {}).get("cn") 97 | items = info.get("items") 98 | for item in items: 99 | if pos >= start_pos: 100 | ret_list.append(self.__dict_item(item, weekday)) 101 | pos += 1 102 | if pos >= start_pos + self._page_num: 103 | break 104 | 105 | return ret_list 106 | -------------------------------------------------------------------------------- /app/media/doubanapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .apiv2 import DoubanApi 2 | from .webapi import DoubanWeb 3 | -------------------------------------------------------------------------------- /app/media/meta/__init__.py: -------------------------------------------------------------------------------- 1 | from .metainfo import MetaInfo 2 | from .metaanime import MetaAnime 3 | from ._base import MetaBase 4 | from .metavideo import MetaVideo 5 | from .release_groups import ReleaseGroupsMatcher 6 | -------------------------------------------------------------------------------- /app/media/meta/metainfo.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import regex as re 3 | 4 | import log 5 | from app.helper import WordsHelper 6 | from app.media.meta.metaanime import MetaAnime 7 | from app.media.meta.metavideo import MetaVideo 8 | from app.utils.types import MediaType 9 | from config import RMT_MEDIAEXT 10 | 11 | 12 | def MetaInfo(title, subtitle=None, mtype=None): 13 | """ 14 | 媒体整理入口,根据名称和副标题,判断是哪种类型的识别,返回对应对象 15 | :param title: 标题、种子名、文件名 16 | :param subtitle: 副标题、描述 17 | :param mtype: 指定识别类型,为空则自动识别类型 18 | :return: MetaAnime、MetaVideo 19 | """ 20 | 21 | # 应用自定义识别词 22 | title, msg, used_info = WordsHelper().process(title) 23 | if subtitle: 24 | subtitle, _, _ = WordsHelper().process(subtitle) 25 | 26 | if msg: 27 | for msg_item in msg: 28 | log.warn("【Meta】%s" % msg_item) 29 | 30 | # 判断是否处理文件 31 | if title and os.path.splitext(title)[-1] in RMT_MEDIAEXT: 32 | fileflag = True 33 | else: 34 | fileflag = False 35 | 36 | if mtype == MediaType.ANIME or is_anime(title): 37 | meta_info = MetaAnime(title, subtitle, fileflag) 38 | else: 39 | meta_info = MetaVideo(title, subtitle, fileflag) 40 | 41 | meta_info.ignored_words = used_info.get("ignored") 42 | meta_info.replaced_words = used_info.get("replaced") 43 | meta_info.offset_words = used_info.get("offset") 44 | 45 | return meta_info 46 | 47 | 48 | def is_anime(name): 49 | """ 50 | 判断是否为动漫 51 | :param name: 名称 52 | :return: 是否动漫 53 | """ 54 | if not name: 55 | return False 56 | if re.search(r'【[+0-9XVPI-]+】\s*【', name, re.IGNORECASE): 57 | return True 58 | if re.search(r'\s+-\s+[\dv]{1,4}\s+', name, re.IGNORECASE): 59 | return True 60 | if re.search(r"S\d{2}\s*-\s*S\d{2}|S\d{2}|\s+S\d{1,2}|EP?\d{2,4}\s*-\s*EP?\d{2,4}|EP?\d{2,4}|\s+EP?\d{1,4}", name, 61 | re.IGNORECASE): 62 | return False 63 | if re.search(r'\[[+0-9XVPI-]+]\s*\[', name, re.IGNORECASE): 64 | return True 65 | return False 66 | -------------------------------------------------------------------------------- /app/media/tmdbv3api/__init__.py: -------------------------------------------------------------------------------- 1 | from .tmdb import TMDb 2 | from .exceptions import TMDbException 3 | from .objs.movie import Movie 4 | from .objs.search import Search 5 | from .objs.tv import TV 6 | from .objs.person import Person 7 | from .objs.find import Find 8 | from .objs.discover import Discover 9 | from .objs.trending import Trending 10 | from .objs.episode import Episode 11 | from .objs.genre import Genre 12 | -------------------------------------------------------------------------------- /app/media/tmdbv3api/as_obj.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import sys 3 | 4 | from app.media.tmdbv3api.exceptions import TMDbException 5 | 6 | 7 | class AsObj: 8 | def __init__(self, **entries): 9 | if "success" in entries and entries["success"] is False: 10 | raise TMDbException(entries["status_message"]) 11 | for key, value in entries.items(): 12 | if isinstance(value, list): 13 | value = [AsObj(**item) if isinstance(item, dict) else item for item in value] 14 | if isinstance(value, dict): 15 | value = AsObj(**value) 16 | setattr(self, key, value) 17 | 18 | def __delitem__(self, key): 19 | return delattr(self, key) 20 | 21 | def __getitem__(self, key): 22 | return getattr(self, key) 23 | 24 | def __iter__(self): 25 | return iter(self.__dict__) 26 | 27 | def __len__(self): 28 | return len(self.__dict__) 29 | 30 | def __repr__(self): 31 | return str(self.__dict__) 32 | 33 | def __setitem__(self, key, value): 34 | return setattr(self, key, value) 35 | 36 | def __str__(self): 37 | return str(self.__dict__) 38 | 39 | if sys.version_info >= (3, 8): 40 | def __reversed__(self): 41 | return reversed(self.__dict__) 42 | 43 | if sys.version_info >= (3, 9): 44 | def __class_getitem__(cls, key): 45 | return cls.__dict__.__class_getitem__(key) 46 | 47 | def __ior__(self, value): 48 | return self.__dict__.__ior__(value) 49 | 50 | def __or__(self, value): 51 | return self.__dict__.__or__(value) 52 | 53 | def clear(self): 54 | return self.__dict__.clear() 55 | 56 | def copy(self): 57 | return AsObj(**self.__dict__.copy()) 58 | 59 | def fromkeys(self, keys, value=None): 60 | return AsObj(**self.__dict__.fromkeys(keys, value)) 61 | 62 | def get(self, key, value=None): 63 | return self.__dict__.get(key, value) 64 | 65 | def items(self): 66 | return self.__dict__.items() 67 | 68 | def keys(self): 69 | return self.__dict__.keys() 70 | 71 | def pop(self, key, value=None): 72 | return self.__dict__.pop(key, value) 73 | 74 | def popitem(self): 75 | return self.__dict__.popitem() 76 | 77 | def setdefault(self, key, value=None): 78 | return self.__dict__.setdefault(key, value) 79 | 80 | def update(self, entries): 81 | return self.__dict__.update(entries) 82 | 83 | def values(self): 84 | return self.__dict__.values() 85 | -------------------------------------------------------------------------------- /app/media/tmdbv3api/exceptions.py: -------------------------------------------------------------------------------- 1 | class TMDbException(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /app/media/tmdbv3api/objs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/app/media/tmdbv3api/objs/__init__.py -------------------------------------------------------------------------------- /app/media/tmdbv3api/objs/discover.py: -------------------------------------------------------------------------------- 1 | from app.media.tmdbv3api.tmdb import TMDb 2 | 3 | try: 4 | from urllib import urlencode 5 | except ImportError: 6 | from urllib.parse import urlencode 7 | 8 | 9 | class Discover(TMDb): 10 | _urls = { 11 | "movies": "/discover/movie", 12 | "tvs": "/discover/tv" 13 | } 14 | 15 | def discover_movies(self, params, page=1): 16 | """ 17 | Discover movies by different types of data like average rating, number of votes, genres and certifications. 18 | :param params: dict 19 | :param page: int 20 | :return: 21 | """ 22 | if not params: 23 | params = {} 24 | if page: 25 | params.update({"page": page}) 26 | return self._get_obj( 27 | self._call( 28 | self._urls["movies"], 29 | urlencode(params) 30 | ), 31 | "results" 32 | ) 33 | 34 | def discover_tv_shows(self, params, page=1): 35 | """ 36 | Discover TV shows by different types of data like average rating, number of votes, genres, 37 | the network they aired on and air dates. 38 | :param params: dict 39 | :param page: int 40 | :return: 41 | """ 42 | if not params: 43 | params = {} 44 | if page: 45 | params.update({"page": page}) 46 | return self._get_obj( 47 | self._call( 48 | self._urls["tvs"], 49 | urlencode(params) 50 | ), 51 | "results" 52 | ) 53 | -------------------------------------------------------------------------------- /app/media/tmdbv3api/objs/episode.py: -------------------------------------------------------------------------------- 1 | from app.media.tmdbv3api.tmdb import TMDb 2 | 3 | 4 | class Episode(TMDb): 5 | _urls = { 6 | "images": "/tv/%s/season/%s/episode/%s/images" 7 | } 8 | 9 | def images(self, tv_id, season_num, episode_num, include_image_language=None): 10 | """ 11 | Get the images that belong to a TV episode. 12 | :param tv_id: int 13 | :param season_num: int 14 | :param episode_num: int 15 | :param include_image_language: str 16 | :return: 17 | """ 18 | return self._get_obj( 19 | self._call( 20 | self._urls["images"] % (tv_id, season_num, episode_num), 21 | "include_image_language=%s" % include_image_language if include_image_language else "", 22 | ), 23 | "stills" 24 | ) 25 | -------------------------------------------------------------------------------- /app/media/tmdbv3api/objs/find.py: -------------------------------------------------------------------------------- 1 | from app.media.tmdbv3api.tmdb import TMDb 2 | 3 | 4 | class Find(TMDb): 5 | _urls = { 6 | "find": "/find/%s" 7 | } 8 | 9 | def find_by_imdbid(self, imdbid): 10 | return self._call( 11 | self._urls["find"] % imdbid, 12 | "external_source=imdb_id") 13 | -------------------------------------------------------------------------------- /app/media/tmdbv3api/objs/genre.py: -------------------------------------------------------------------------------- 1 | from app.media.tmdbv3api.tmdb import TMDb 2 | 3 | 4 | class Genre(TMDb): 5 | _urls = { 6 | "movie_list": "/genre/movie/list", 7 | "tv_list": "/genre/tv/list" 8 | } 9 | 10 | def movie_list(self): 11 | """ 12 | Get the list of official genres for movies. 13 | :return: 14 | """ 15 | return self._get_obj(self._call(self._urls["movie_list"], ""), "genres") 16 | 17 | def tv_list(self): 18 | """ 19 | Get the list of official genres for TV shows. 20 | :return: 21 | """ 22 | return self._get_obj(self._call(self._urls["tv_list"], ""), "genres") 23 | -------------------------------------------------------------------------------- /app/media/tmdbv3api/objs/search.py: -------------------------------------------------------------------------------- 1 | from app.media.tmdbv3api.tmdb import TMDb 2 | 3 | try: 4 | from urllib import urlencode 5 | except ImportError: 6 | from urllib.parse import urlencode 7 | 8 | 9 | class Search(TMDb): 10 | _urls = { 11 | "companies": "/search/company", 12 | "collections": "/search/collection", 13 | "keywords": "/search/keyword", 14 | "movies": "/search/movie", 15 | "multi": "/search/multi", 16 | "people": "/search/person", 17 | "tv_shows": "/search/tv", 18 | } 19 | 20 | def companies(self, params): 21 | """ 22 | Search for movies. 23 | :param params: 24 | :return: 25 | """ 26 | return self._get_obj(self._call(self._urls["companies"], urlencode(params))) 27 | 28 | def collections(self, params): 29 | """ 30 | Search for movies. 31 | :param params: 32 | :return: 33 | """ 34 | return self._get_obj(self._call(self._urls["collections"], urlencode(params))) 35 | 36 | def keywords(self, params): 37 | """ 38 | Search for movies. 39 | :param params: 40 | :return: 41 | """ 42 | return self._get_obj(self._call(self._urls["keywords"], urlencode(params))) 43 | 44 | def movies(self, params): 45 | """ 46 | Search for movies. 47 | :param params: 48 | :return: 49 | """ 50 | return self._get_obj(self._call(self._urls["movies"], urlencode(params))) 51 | 52 | def multi(self, params): 53 | """ 54 | Search for movies. 55 | :param params: 56 | :return: 57 | """ 58 | return self._get_obj(self._call(self._urls["multi"], urlencode(params))) 59 | 60 | def people(self, params): 61 | """ 62 | Search for movies. 63 | :param params: 64 | :return: 65 | """ 66 | return self._get_obj(self._call(self._urls["people"], urlencode(params))) 67 | 68 | def tv_shows(self, params): 69 | """ 70 | Search for movies. 71 | :param params: 72 | :return: 73 | """ 74 | return self._get_obj(self._call(self._urls["tv_shows"], urlencode(params))) 75 | -------------------------------------------------------------------------------- /app/media/tmdbv3api/objs/trending.py: -------------------------------------------------------------------------------- 1 | from app.media.tmdbv3api.tmdb import TMDb 2 | 3 | 4 | class Trending(TMDb): 5 | _urls = {"trending": "/trending/%s/%s"} 6 | 7 | def _trending(self, media_type="all", time_window="day", page=1): 8 | return self._get_obj( 9 | self._call( 10 | self._urls["trending"] % (media_type, time_window), 11 | "page=%s" % page 12 | ) 13 | ) 14 | 15 | def all_day(self, page=1): 16 | """ 17 | Get all daily trending 18 | :param page: int 19 | :return: 20 | """ 21 | return self._trending(media_type="all", time_window="day", page=page) 22 | 23 | def all_week(self, page=1): 24 | """ 25 | Get all weekly trending 26 | :param page: int 27 | :return: 28 | """ 29 | return self._trending(media_type="all", time_window="week", page=page) 30 | 31 | def movie_day(self, page=1): 32 | """ 33 | Get movie daily trending 34 | :param page: int 35 | :return: 36 | """ 37 | return self._trending(media_type="movie", time_window="day", page=page) 38 | 39 | def movie_week(self, page=1): 40 | """ 41 | Get movie weekly trending 42 | :param page: int 43 | :return: 44 | """ 45 | return self._trending(media_type="movie", time_window="week", page=page) 46 | 47 | def tv_day(self, page=1): 48 | """ 49 | Get tv daily trending 50 | :param page: int 51 | :return: 52 | """ 53 | return self._trending(media_type="tv", time_window="day", page=page) 54 | 55 | def tv_week(self, page=1): 56 | """ 57 | Get tv weekly trending 58 | :param page: int 59 | :return: 60 | """ 61 | return self._trending(media_type="tv", time_window="week", page=page) 62 | 63 | def person_day(self, page=1): 64 | """ 65 | Get person daily trending 66 | :param page: int 67 | :return: 68 | """ 69 | return self._trending(media_type="person", time_window="day", page=page) 70 | 71 | def person_week(self, page=1): 72 | """ 73 | Get person weekly trending 74 | :param page: int 75 | :return: 76 | """ 77 | return self._trending(media_type="person", time_window="week", page=page) 78 | -------------------------------------------------------------------------------- /app/mediaserver/__init__.py: -------------------------------------------------------------------------------- 1 | from .media_server import MediaServer 2 | from .webhook_event import WebhookEvent 3 | -------------------------------------------------------------------------------- /app/mediaserver/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/app/mediaserver/client/__init__.py -------------------------------------------------------------------------------- /app/mediaserver/client/_base.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class _IMediaClient(metaclass=ABCMeta): 5 | 6 | @abstractmethod 7 | def match(self, ctype): 8 | """ 9 | 匹配实例 10 | """ 11 | pass 12 | 13 | @abstractmethod 14 | def get_status(self): 15 | """ 16 | 检查连通性 17 | """ 18 | pass 19 | 20 | @abstractmethod 21 | def get_user_count(self): 22 | """ 23 | 获得用户数量 24 | """ 25 | pass 26 | 27 | @abstractmethod 28 | def get_activity_log(self, num): 29 | """ 30 | 获取Emby活动记录 31 | """ 32 | pass 33 | 34 | @abstractmethod 35 | def get_medias_count(self): 36 | """ 37 | 获得电影、电视剧、动漫媒体数量 38 | :return: MovieCount SeriesCount SongCount 39 | """ 40 | pass 41 | 42 | @abstractmethod 43 | def get_movies(self, title, year): 44 | """ 45 | 根据标题和年份,检查电影是否在存在,存在则返回列表 46 | :param title: 标题 47 | :param year: 年份,可以为空,为空时不按年份过滤 48 | :return: 含title、year属性的字典列表 49 | """ 50 | pass 51 | 52 | @abstractmethod 53 | def get_no_exists_episodes(self, meta_info, season, total_num): 54 | """ 55 | 根据标题、年份、季、总集数,查询缺少哪几集 56 | :param meta_info: 已识别的需要查询的媒体信息 57 | :param season: 季号,数字 58 | :param total_num: 该季的总集数 59 | :return: 该季不存在的集号列表 60 | """ 61 | pass 62 | 63 | @abstractmethod 64 | def get_image_by_id(self, item_id, image_type): 65 | """ 66 | 根据ItemId查询图片地址 67 | :param item_id: 在服务器中的ID 68 | :param image_type: 图片的类弄地,poster或者backdrop等 69 | :return: 图片对应在TMDB中的URL 70 | """ 71 | pass 72 | 73 | @abstractmethod 74 | def refresh_root_library(self): 75 | """ 76 | 刷新整个媒体库 77 | """ 78 | pass 79 | 80 | @abstractmethod 81 | def refresh_library_by_items(self, items): 82 | """ 83 | 按类型、名称、年份来刷新媒体库 84 | :param items: 已识别的需要刷新媒体库的媒体信息列表 85 | """ 86 | pass 87 | 88 | @abstractmethod 89 | def get_libraries(self): 90 | """ 91 | 获取媒体服务器所有媒体库列表 92 | """ 93 | pass 94 | 95 | @abstractmethod 96 | def get_items(self, parent): 97 | """ 98 | 获取媒体库中的所有媒体 99 | :param parent: 上一级的ID 100 | """ 101 | pass 102 | 103 | @abstractmethod 104 | def get_playing_sessions(self): 105 | """ 106 | 获取正在播放的会话 107 | """ 108 | pass 109 | -------------------------------------------------------------------------------- /app/message/__init__.py: -------------------------------------------------------------------------------- 1 | from .message import Message 2 | from .message_center import MessageCenter 3 | -------------------------------------------------------------------------------- /app/message/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/app/message/client/__init__.py -------------------------------------------------------------------------------- /app/message/client/_base.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class _IMessageClient(metaclass=ABCMeta): 5 | 6 | @abstractmethod 7 | def match(self, ctype): 8 | """ 9 | 匹配实例 10 | """ 11 | pass 12 | 13 | @abstractmethod 14 | def send_msg(self, title, text, image, url, user_id): 15 | """ 16 | 消息发送入口,支持文本、图片、链接跳转、指定发送对象 17 | :param title: 消息标题 18 | :param text: 消息内容 19 | :param image: 图片地址 20 | :param url: 点击消息跳转URL 21 | :param user_id: 消息发送对象的ID,为空则发给所有人 22 | :return: 发送状态,错误信息 23 | """ 24 | pass 25 | 26 | @abstractmethod 27 | def send_list_msg(self, medias: list, user_id="", title="", url=""): 28 | """ 29 | 发送列表类消息 30 | :param title: 消息标题 31 | :param medias: 媒体列表 32 | :param user_id: 消息发送对象的ID,为空则发给所有人 33 | :param url: 跳转链接地址 34 | """ 35 | pass 36 | -------------------------------------------------------------------------------- /app/message/client/bark.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import quote_plus 2 | 3 | from app.message.client._base import _IMessageClient 4 | from app.utils import RequestUtils, StringUtils, ExceptionUtils 5 | 6 | 7 | class Bark(_IMessageClient): 8 | schema = "bark" 9 | 10 | _server = None 11 | _apikey = None 12 | _params = None 13 | _client_config = {} 14 | 15 | def __init__(self, config): 16 | self._client_config = config 17 | self.init_config() 18 | 19 | def init_config(self): 20 | if self._client_config: 21 | self._server = StringUtils.get_base_url(self._client_config.get('server')) 22 | self._apikey = self._client_config.get('apikey') 23 | self._params = self._client_config.get('params') 24 | 25 | @classmethod 26 | def match(cls, ctype): 27 | return True if ctype == cls.schema else False 28 | 29 | def send_msg(self, title, text="", image="", url="", user_id=""): 30 | """ 31 | 发送Bark消息 32 | :param title: 消息标题 33 | :param text: 消息内容 34 | :param image: 未使用 35 | :param url: 未使用 36 | :param user_id: 未使用 37 | :return: 发送状态、错误信息 38 | """ 39 | if not title and not text: 40 | return False, "标题和内容不能同时为空" 41 | try: 42 | if not self._server or not self._apikey: 43 | return False, "参数未配置" 44 | sc_url = "%s/%s/%s/%s" % (self._server, self._apikey, quote_plus(title), quote_plus(text)) 45 | if self._params: 46 | sc_url = "%s?%s" % (sc_url, self._params) 47 | res = RequestUtils().post_res(sc_url) 48 | if res: 49 | ret_json = res.json() 50 | code = ret_json['code'] 51 | message = ret_json['message'] 52 | if code == 200: 53 | return True, message 54 | else: 55 | return False, message 56 | else: 57 | return False, "未获取到返回信息" 58 | except Exception as msg_e: 59 | ExceptionUtils.exception_traceback(msg_e) 60 | return False, str(msg_e) 61 | 62 | def send_list_msg(self, **kwargs): 63 | pass 64 | -------------------------------------------------------------------------------- /app/message/client/chanify.py: -------------------------------------------------------------------------------- 1 | from urllib import parse 2 | 3 | from app.message.client._base import _IMessageClient 4 | from app.utils import RequestUtils, StringUtils, ExceptionUtils 5 | 6 | 7 | class Chanify(_IMessageClient): 8 | schema = "chanify" 9 | 10 | _server = None 11 | _token = None 12 | _client_config = {} 13 | 14 | def __init__(self, config): 15 | self._client_config = config 16 | self.init_config() 17 | 18 | def init_config(self): 19 | if self._client_config: 20 | self._server = StringUtils.get_base_url(self._client_config.get('server')) 21 | self._token = self._client_config.get('token') 22 | 23 | @classmethod 24 | def match(cls, ctype): 25 | return True if ctype == cls.schema else False 26 | 27 | def send_msg(self, title, text="", image="", url="", user_id=""): 28 | """ 29 | 发送Bark消息 30 | :param title: 消息标题 31 | :param text: 消息内容 32 | :param image: 未使用 33 | :param url: 未使用 34 | :param user_id: 未使用 35 | :return: 发送状态、错误信息 36 | """ 37 | if not title and not text: 38 | return False, "标题和内容不能同时为空" 39 | try: 40 | if not self._server or not self._token: 41 | return False, "参数未配置" 42 | sc_url = "%s/v1/sender/%s" % (self._server, self._token) 43 | # 发送文本 44 | data = parse.urlencode({ 45 | 'title': title, 46 | 'text': text 47 | }).encode() 48 | res = RequestUtils().post_res(sc_url, params=data) 49 | if res: 50 | if res.status_code == 200: 51 | return True, "发送成功" 52 | else: 53 | return False, "错误码:%s" % res.status_code 54 | else: 55 | return False, "未获取到返回信息" 56 | except Exception as msg_e: 57 | ExceptionUtils.exception_traceback(msg_e) 58 | return False, str(msg_e) 59 | 60 | def send_list_msg(self, **kwargs): 61 | pass 62 | -------------------------------------------------------------------------------- /app/message/client/gotify.py: -------------------------------------------------------------------------------- 1 | from app.message.client._base import _IMessageClient 2 | from app.utils import RequestUtils, StringUtils, ExceptionUtils 3 | 4 | 5 | class Gotify(_IMessageClient): 6 | schema = "gotify" 7 | 8 | _server = None 9 | _token = None 10 | _priority = None 11 | _client_config = {} 12 | 13 | def __init__(self, config): 14 | self._client_config = config 15 | self.init_config() 16 | 17 | def init_config(self): 18 | if self._client_config: 19 | self._server = StringUtils.get_base_url(self._client_config.get('server')) 20 | self._token = self._client_config.get('token') 21 | try: 22 | self._priority = int(self._client_config.get('priority')) 23 | except Exception as e: 24 | self._priority = 8 25 | ExceptionUtils.exception_traceback(e) 26 | 27 | @classmethod 28 | def match(cls, ctype): 29 | return True if ctype == cls.schema else False 30 | 31 | def send_msg(self, title, text="", image="", url="", user_id=""): 32 | """ 33 | 发送Bark消息 34 | :param title: 消息标题 35 | :param text: 消息内容 36 | :param image: 未使用 37 | :param url: 点击消息跳转URL, 为空时则没有任何动作 38 | :param user_id: 未使用 39 | :return: 发送状态、错误信息 40 | """ 41 | if not title and not text: 42 | return False, "标题和内容不能同时为空" 43 | try: 44 | if not self._server or not self._token: 45 | return False, "参数未配置" 46 | sc_url = "%s/message?token=%s" % (self._server, self._token) 47 | sc_data = { 48 | "title": title, 49 | "message": text, 50 | "priority": self._priority, 51 | "extras": { 52 | "client::notification": { 53 | "click": { 54 | "url": url 55 | } 56 | }, 57 | } 58 | } 59 | res = RequestUtils(content_type="application/json").post_res(sc_url, json=sc_data) 60 | if res and res.status_code == 200: 61 | return True, "发送成功" 62 | elif res: 63 | return False, f"错误码:{res.status_code}" 64 | else: 65 | return False, "未获取到返回信息" 66 | except Exception as msg_e: 67 | ExceptionUtils.exception_traceback(msg_e) 68 | return False, str(msg_e) 69 | 70 | def send_list_msg(self, **kwargs): 71 | pass 72 | -------------------------------------------------------------------------------- /app/message/client/iyuu.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlencode 2 | 3 | from app.message.client._base import _IMessageClient 4 | from app.utils import RequestUtils, ExceptionUtils 5 | 6 | 7 | class IyuuMsg(_IMessageClient): 8 | schema = "iyuu" 9 | 10 | _token = None 11 | _client_config = {} 12 | 13 | def __init__(self, config): 14 | self._client_config = config 15 | self.init_config() 16 | 17 | def init_config(self): 18 | if self._client_config: 19 | self._token = self._client_config.get('token') 20 | 21 | @classmethod 22 | def match(cls, ctype): 23 | return True if ctype == cls.schema else False 24 | 25 | def send_msg(self, title, text="", image="", url="", user_id=""): 26 | """ 27 | 发送爱语飞飞消息 28 | :param title: 消息标题 29 | :param text: 消息内容 30 | :param image: 未使用 31 | :param url: 未使用 32 | :param user_id: 未使用 33 | """ 34 | if not title and not text: 35 | return False, "标题和内容不能同时为空" 36 | if not self._token: 37 | return False, "参数未配置" 38 | try: 39 | sc_url = "http://iyuu.cn/%s.send?%s" % (self._token, urlencode({"text": title, "desp": text})) 40 | res = RequestUtils().get_res(sc_url) 41 | if res: 42 | ret_json = res.json() 43 | errno = ret_json.get('errcode') 44 | error = ret_json.get('errmsg') 45 | if errno == 0: 46 | return True, error 47 | else: 48 | return False, error 49 | else: 50 | return False, "未获取到返回信息" 51 | except Exception as msg_e: 52 | ExceptionUtils.exception_traceback(msg_e) 53 | return False, str(msg_e) 54 | 55 | def send_list_msg(self, **kwargs): 56 | pass 57 | -------------------------------------------------------------------------------- /app/message/client/pushdeer.py: -------------------------------------------------------------------------------- 1 | from pypushdeer import PushDeer 2 | 3 | from app.message.client._base import _IMessageClient 4 | from app.utils import StringUtils, ExceptionUtils 5 | 6 | 7 | class PushDeerClient(_IMessageClient): 8 | schema = "pushdeer" 9 | 10 | _server = None 11 | _apikey = None 12 | _client_config = {} 13 | 14 | def __init__(self, config): 15 | self._client_config = config 16 | self.init_config() 17 | 18 | def init_config(self): 19 | if self._client_config: 20 | self._server = StringUtils.get_base_url(self._client_config.get('server')) 21 | self._apikey = self._client_config.get('apikey') 22 | 23 | @classmethod 24 | def match(cls, ctype): 25 | return True if ctype == cls.schema else False 26 | 27 | def send_msg(self, title, text="", image="", url="", user_id=""): 28 | """ 29 | 发送PushDeer消息 30 | :param title: 消息标题 31 | :param text: 消息内容 32 | :param image: 未使用 33 | :param url: 未使用 34 | :param user_id: 未使用 35 | :return: 发送状态、错误信息 36 | """ 37 | if not title and not text: 38 | return False, "标题和内容不能同时为空" 39 | try: 40 | if not self._server or not self._apikey: 41 | return False, "参数未配置" 42 | pushdeer = PushDeer(server=self._server, pushkey=self._apikey) 43 | res = pushdeer.send_markdown(title, desp=text) 44 | if res: 45 | return True, "成功" 46 | else: 47 | return False, "失败" 48 | except Exception as msg_e: 49 | ExceptionUtils.exception_traceback(msg_e) 50 | return False, str(msg_e) 51 | 52 | def send_list_msg(self, **kwargs): 53 | pass 54 | -------------------------------------------------------------------------------- /app/message/client/pushplus.py: -------------------------------------------------------------------------------- 1 | import time 2 | from urllib.parse import urlencode 3 | 4 | from app.message.client._base import _IMessageClient 5 | from app.utils import RequestUtils, ExceptionUtils 6 | 7 | 8 | class PushPlus(_IMessageClient): 9 | schema = "pushplus" 10 | 11 | _token = None 12 | _topic = None 13 | _channel = None 14 | _webhook = None 15 | _client_config = {} 16 | 17 | def __init__(self, config): 18 | self._client_config = config 19 | self.init_config() 20 | 21 | def init_config(self): 22 | if self._client_config: 23 | self._token = self._client_config.get('token') 24 | self._topic = self._client_config.get('topic') 25 | self._channel = self._client_config.get('channel') 26 | self._webhook = self._client_config.get('webhook') 27 | 28 | @classmethod 29 | def match(cls, ctype): 30 | return True if ctype == cls.schema else False 31 | 32 | def send_msg(self, title, text="", image="", url="", user_id=""): 33 | """ 34 | 发送ServerChan消息 35 | :param title: 消息标题 36 | :param text: 消息内容 37 | :param image: 未使用 38 | :param url: 未使用 39 | :param user_id: 未使用 40 | """ 41 | if not title and not text: 42 | return False, "标题和内容不能同时为空" 43 | if not text: 44 | text = "无" 45 | if not self._token or not self._channel: 46 | return False, "参数未配置" 47 | try: 48 | values = { 49 | "token": self._token, 50 | "channel": self._channel, 51 | "topic": self._topic, 52 | "webhook": self._webhook, 53 | "title": title, 54 | "content": text, 55 | "timestamp": time.time_ns() + 60 56 | } 57 | sc_url = "http://www.pushplus.plus/send?%s" % urlencode(values) 58 | res = RequestUtils().get_res(sc_url) 59 | if res: 60 | ret_json = res.json() 61 | code = ret_json.get("code") 62 | msg = ret_json.get("msg") 63 | if code == 200: 64 | return True, msg 65 | else: 66 | return False, msg 67 | else: 68 | return False, "未获取到返回信息" 69 | except Exception as msg_e: 70 | ExceptionUtils.exception_traceback(msg_e) 71 | return False, str(msg_e) 72 | 73 | def send_list_msg(self, **kwargs): 74 | pass 75 | -------------------------------------------------------------------------------- /app/message/client/serverchan.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlencode 2 | 3 | from app.message.client._base import _IMessageClient 4 | from app.utils import RequestUtils, ExceptionUtils 5 | 6 | 7 | class ServerChan(_IMessageClient): 8 | schema = "serverchan" 9 | 10 | _sckey = None 11 | _client_config = {} 12 | 13 | def __init__(self, config): 14 | self._client_config = config 15 | self.init_config() 16 | 17 | def init_config(self): 18 | if self._client_config: 19 | self._sckey = self._client_config.get('sckey') 20 | 21 | @classmethod 22 | def match(cls, ctype): 23 | return True if ctype == cls.schema else False 24 | 25 | def send_msg(self, title, text="", image="", url="", user_id=""): 26 | """ 27 | 发送ServerChan消息 28 | :param title: 消息标题 29 | :param text: 消息内容 30 | :param image: 未使用 31 | :param url: 未使用 32 | :param user_id: 未使用 33 | """ 34 | if not title and not text: 35 | return False, "标题和内容不能同时为空" 36 | if not self._sckey: 37 | return False, "参数未配置" 38 | try: 39 | sc_url = "https://sctapi.ftqq.com/%s.send?%s" % (self._sckey, urlencode({"title": title, "desp": text})) 40 | res = RequestUtils().get_res(sc_url) 41 | if res: 42 | ret_json = res.json() 43 | errno = ret_json.get('code') 44 | error = ret_json.get('message') 45 | if errno == 0: 46 | return True, error 47 | else: 48 | return False, error 49 | else: 50 | return False, "未获取到返回信息" 51 | except Exception as msg_e: 52 | ExceptionUtils.exception_traceback(msg_e) 53 | return False, str(msg_e) 54 | 55 | def send_list_msg(self, **kwargs): 56 | pass 57 | -------------------------------------------------------------------------------- /app/message/message_center.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | from collections import deque 4 | 5 | from app.utils.commons import singleton 6 | 7 | 8 | @singleton 9 | class MessageCenter: 10 | _message_queue = deque(maxlen=50) 11 | _message_index = 0 12 | 13 | def __init__(self): 14 | pass 15 | 16 | def insert_system_message(self, level, title, content=None): 17 | """ 18 | 新增系统消息 19 | :param level: 级别 20 | :param title: 标题 21 | :param content: 内容 22 | """ 23 | if not level or not title: 24 | return 25 | if not content and title.find(":") != -1: 26 | strings = title.split(":") 27 | if strings and len(strings) > 1: 28 | title = strings[0] 29 | content = strings[1] 30 | title = title.replace("\n", "
").strip() if title else "" 31 | content = content.replace("\n", "
").strip() if content else "" 32 | self.__append_message_queue(level, title, content) 33 | 34 | def __append_message_queue(self, level, title, content): 35 | """ 36 | 将消息增加到队列 37 | """ 38 | self._message_queue.appendleft({"level": level, 39 | "title": title, 40 | "content": content, 41 | "time": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}) 42 | 43 | def get_system_messages(self, num=20, lst_time=None): 44 | """ 45 | 查询系统消息 46 | :param num:条数 47 | :param lst_time: 最后时间 48 | """ 49 | if not lst_time: 50 | return list(self._message_queue)[-num:] 51 | else: 52 | ret_messages = [] 53 | for message in list(self._message_queue): 54 | if (datetime.datetime.strptime(message.get("time"), '%Y-%m-%d %H:%M:%S') - datetime.datetime.strptime( 55 | lst_time, '%Y-%m-%d %H:%M:%S')).seconds > 0: 56 | ret_messages.append(message) 57 | else: 58 | break 59 | return ret_messages 60 | -------------------------------------------------------------------------------- /app/sites/__init__.py: -------------------------------------------------------------------------------- 1 | from app.sites.site_user_info_factory import SiteUserInfoFactory 2 | from .sites import Sites 3 | from .sitecookie import SiteCookie 4 | -------------------------------------------------------------------------------- /app/sites/siteuserinfo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/app/sites/siteuserinfo/__init__.py -------------------------------------------------------------------------------- /app/sites/siteuserinfo/nexus_project.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | from app.sites.siteuserinfo._base import SITE_BASE_ORDER 5 | from app.sites.siteuserinfo.nexus_php import NexusPhpSiteUserInfo 6 | from app.utils.types import SiteSchema 7 | 8 | 9 | class NexusProjectSiteUserInfo(NexusPhpSiteUserInfo): 10 | schema = SiteSchema.NexusProject 11 | order = SITE_BASE_ORDER + 25 12 | 13 | @classmethod 14 | def match(cls, html_text): 15 | return 'Nexus Project' in html_text 16 | 17 | def _parse_site_page(self, html_text): 18 | html_text = self._prepare_html_text(html_text) 19 | 20 | user_detail = re.search(r"userdetails.php\?id=(\d+)", html_text) 21 | if user_detail and user_detail.group().strip(): 22 | self._user_detail_page = user_detail.group().strip().lstrip('/') 23 | self.userid = user_detail.group(1) 24 | 25 | self._torrent_seeding_page = f"viewusertorrents.php?id={self.userid}&show=seeding" 26 | -------------------------------------------------------------------------------- /app/sites/siteuserinfo/nexus_rabbit.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | 4 | from lxml import etree 5 | 6 | from app.sites.siteuserinfo._base import SITE_BASE_ORDER 7 | from app.sites.siteuserinfo.nexus_php import NexusPhpSiteUserInfo 8 | from app.utils.exception_utils import ExceptionUtils 9 | from app.utils.types import SiteSchema 10 | 11 | 12 | class NexusRabbitSiteUserInfo(NexusPhpSiteUserInfo): 13 | schema = SiteSchema.NexusRabbit 14 | order = SITE_BASE_ORDER + 5 15 | 16 | @classmethod 17 | def match(cls, html_text): 18 | html = etree.HTML(html_text) 19 | if not html: 20 | return False 21 | 22 | printable_text = html.xpath("string(.)") if html else "" 23 | return 'Style by Rabbit' in printable_text 24 | 25 | def _parse_site_page(self, html_text): 26 | super()._parse_site_page(html_text) 27 | self._torrent_seeding_page = f"getusertorrentlistajax.php?page=1&limit=5000000&type=seeding&uid={self.userid}" 28 | self._torrent_seeding_headers = {"Accept": "application/json, text/javascript, */*; q=0.01"} 29 | 30 | def _parse_user_torrent_seeding_info(self, html_text, multi_page=False): 31 | """ 32 | 做种相关信息 33 | :param html_text: 34 | :param multi_page: 是否多页数据 35 | :return: 下页地址 36 | """ 37 | 38 | try: 39 | torrents = json.loads(html_text).get('data') 40 | except Exception as e: 41 | ExceptionUtils.exception_traceback(e) 42 | return 43 | 44 | page_seeding_size = 0 45 | page_seeding_info = [] 46 | 47 | page_seeding = len(torrents) 48 | for torrent in torrents: 49 | seeders = int(torrent.get('seeders', 0)) 50 | size = int(torrent.get('size', 0)) 51 | page_seeding_size += int(torrent.get('size', 0)) 52 | 53 | page_seeding_info.append([seeders, size]) 54 | 55 | self.seeding += page_seeding 56 | self.seeding_size += page_seeding_size 57 | self.seeding_info.extend(page_seeding_info) 58 | -------------------------------------------------------------------------------- /app/sites/siteuserinfo/small_horse.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | from lxml import etree 5 | 6 | from app.sites.siteuserinfo._base import _ISiteUserInfo, SITE_BASE_ORDER 7 | from app.utils import StringUtils 8 | from app.utils.types import SiteSchema 9 | 10 | 11 | class SmallHorseSiteUserInfo(_ISiteUserInfo): 12 | schema = SiteSchema.SmallHorse 13 | order = SITE_BASE_ORDER + 30 14 | 15 | @classmethod 16 | def match(cls, html_text): 17 | return 'Small Horse' in html_text 18 | 19 | def _parse_site_page(self, html_text): 20 | html_text = self._prepare_html_text(html_text) 21 | 22 | user_detail = re.search(r"user.php\?id=(\d+)", html_text) 23 | if user_detail and user_detail.group().strip(): 24 | self._user_detail_page = user_detail.group().strip().lstrip('/') 25 | self.userid = user_detail.group(1) 26 | self._user_traffic_page = f"user.php?id={self.userid}" 27 | 28 | def _parse_user_base_info(self, html_text): 29 | html_text = self._prepare_html_text(html_text) 30 | html = etree.HTML(html_text) 31 | ret = html.xpath('//a[contains(@href, "user.php")]//text()') 32 | if ret: 33 | self.username = str(ret[0]) 34 | 35 | def _parse_user_traffic_info(self, html_text): 36 | """ 37 | 上传/下载/分享率 [做种数/魔力值] 38 | :param html_text: 39 | :return: 40 | """ 41 | html_text = self._prepare_html_text(html_text) 42 | html = etree.HTML(html_text) 43 | tmps = html.xpath('//ul[@class = "stats nobullet"]') 44 | if tmps: 45 | if tmps[1].xpath("li") and tmps[1].xpath("li")[0].xpath("span//text()"): 46 | self.join_at = StringUtils.unify_datetime_str(tmps[1].xpath("li")[0].xpath("span//text()")[0]) 47 | self.upload = StringUtils.num_filesize(str(tmps[1].xpath("li")[2].xpath("text()")[0]).split(":")[1].strip()) 48 | self.download = StringUtils.num_filesize( 49 | str(tmps[1].xpath("li")[3].xpath("text()")[0]).split(":")[1].strip()) 50 | if tmps[1].xpath("li")[4].xpath("span//text()"): 51 | self.ratio = StringUtils.str_float(str(tmps[1].xpath("li")[4].xpath("span//text()")[0]).replace('∞', '0')) 52 | else: 53 | self.ratio = StringUtils.str_float(str(tmps[1].xpath("li")[5].xpath("text()")[0]).split(":")[1]) 54 | self.bonus = StringUtils.str_float(str(tmps[1].xpath("li")[5].xpath("text()")[0]).split(":")[1]) 55 | self.user_level = str(tmps[3].xpath("li")[0].xpath("text()")[0]).split(":")[1].strip() 56 | self.seeding = StringUtils.str_int( 57 | (tmps[4].xpath("li")[5].xpath("text()")[0]).split(":")[1].replace("[", "")) 58 | self.leeching = StringUtils.str_int( 59 | (tmps[4].xpath("li")[6].xpath("text()")[0]).split(":")[1].replace("[", "")) 60 | 61 | def _parse_user_detail_info(self, html_text): 62 | pass 63 | 64 | def _parse_user_torrent_seeding_info(self, html_text, multi_page=False): 65 | pass 66 | 67 | def _parse_message_unread_links(self, html_text, msg_links): 68 | return None 69 | 70 | def _parse_message_content(self, html_text): 71 | return None, None, None 72 | -------------------------------------------------------------------------------- /app/sites/siteuserinfo/tnode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import re 4 | 5 | from app.sites.siteuserinfo._base import _ISiteUserInfo, SITE_BASE_ORDER 6 | from app.utils import StringUtils 7 | from app.utils.types import SiteSchema 8 | 9 | 10 | class TNodeSiteUserInfo(_ISiteUserInfo): 11 | schema = SiteSchema.TNode 12 | order = SITE_BASE_ORDER + 60 13 | 14 | @classmethod 15 | def match(cls, html_text): 16 | return 'Powered By TNode' in html_text 17 | 18 | def _parse_site_page(self, html_text): 19 | html_text = self._prepare_html_text(html_text) 20 | 21 | # 22 | csrf_token = re.search(r'', html_text) 23 | if csrf_token: 24 | self._addition_headers = {'X-CSRF-TOKEN': csrf_token.group(1)} 25 | self._user_detail_page = "api/user/getMainInfo" 26 | self._torrent_seeding_page = "api/user/listTorrentActivity?id=&type=seeding&page=1&size=20000" 27 | 28 | def _parse_logged_in(self, html_text): 29 | """ 30 | 判断是否登录成功, 通过判断是否存在用户信息 31 | 暂时跳过检测,待后续优化 32 | :param html_text: 33 | :return: 34 | """ 35 | return True 36 | 37 | def _parse_user_base_info(self, html_text): 38 | self.username = self.userid 39 | 40 | def _parse_user_traffic_info(self, html_text): 41 | pass 42 | 43 | def _parse_user_detail_info(self, html_text): 44 | detail = json.loads(html_text) 45 | if detail.get("status") != 200: 46 | return 47 | 48 | user_info = detail.get("data", {}) 49 | self.userid = user_info.get("id") 50 | self.username = user_info.get("username") 51 | self.user_level = user_info.get("class", {}).get("name") 52 | self.join_at = user_info.get("regTime", 0) 53 | self.join_at = StringUtils.unify_datetime_str(str(self.join_at)) 54 | 55 | self.upload = user_info.get("upload") 56 | self.download = user_info.get("download") 57 | self.ratio = 0 if self.download <= 0 else round(self.upload / self.download, 3) 58 | self.bonus = user_info.get("bonus") 59 | 60 | self.message_unread = user_info.get("unreadAdmin", 0) + user_info.get("unreadInbox", 0) + user_info.get( 61 | "unreadSystem", 0) 62 | pass 63 | 64 | def _parse_user_torrent_seeding_info(self, html_text, multi_page=False): 65 | """ 66 | 解析用户做种信息 67 | """ 68 | seeding_info = json.loads(html_text) 69 | if seeding_info.get("status") != 200: 70 | return 71 | 72 | torrents = seeding_info.get("data", {}).get("torrents", []) 73 | 74 | page_seeding_size = 0 75 | page_seeding_info = [] 76 | for torrent in torrents: 77 | size = torrent.get("size", 0) 78 | seeders = torrent.get("seeding", 0) 79 | 80 | page_seeding_size += size 81 | page_seeding_info.append([seeders, size]) 82 | 83 | self.seeding += len(torrents) 84 | self.seeding_size += page_seeding_size 85 | self.seeding_info.extend(page_seeding_info) 86 | 87 | # 是否存在下页数据 88 | next_page = None 89 | 90 | return next_page 91 | 92 | def _parse_message_unread_links(self, html_text, msg_links): 93 | return None 94 | 95 | def _parse_message_content(self, html_text): 96 | """ 97 | 系统信息 api/message/listSystem?page=1&size=20 98 | 收件箱信息 api/message/listInbox?page=1&size=20 99 | 管理员信息 api/message/listAdmin?page=1&size=20 100 | :param html_text: 101 | :return: 102 | """ 103 | return None, None, None 104 | -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .dom_utils import DomUtils 2 | from .episode_format import EpisodeFormat 3 | from .http_utils import RequestUtils 4 | from .json_utils import JsonUtils 5 | from .number_utils import NumberUtils 6 | from .path_utils import PathUtils 7 | from .string_utils import StringUtils 8 | from .system_utils import SystemUtils 9 | from .tokens import Tokens 10 | from .torrent import Torrent 11 | from .cache_manager import cacheman, TokenCache, ConfigLoadCache 12 | from .exception_utils import ExceptionUtils 13 | from .rsstitle_utils import RssTitleUtils 14 | -------------------------------------------------------------------------------- /app/utils/cache_manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | 4 | from cacheout import CacheManager, LRUCache, Cache 5 | 6 | CACHES = { 7 | "tmdb_supply": {'maxsize': 200} 8 | } 9 | 10 | cacheman = CacheManager(CACHES, cache_class=LRUCache) 11 | 12 | TokenCache = Cache(maxsize=256, ttl=4*3600, timer=time.time, default=None) 13 | 14 | ConfigLoadCache = Cache(maxsize=1, ttl=10, timer=time.time, default=None) 15 | -------------------------------------------------------------------------------- /app/utils/commons.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import threading 3 | 4 | # 线程锁 5 | lock = threading.RLock() 6 | 7 | # 全局实例 8 | INSTANCES = {} 9 | 10 | 11 | # 单例模式注解 12 | def singleton(cls): 13 | # 创建字典用来保存类的实例对象 14 | global INSTANCES 15 | 16 | def _singleton(*args, **kwargs): 17 | # 先判断这个类有没有对象 18 | if cls not in INSTANCES: 19 | with lock: 20 | if cls not in INSTANCES: 21 | INSTANCES[cls] = cls(*args, **kwargs) 22 | pass 23 | # 将实例对象返回 24 | return INSTANCES[cls] 25 | 26 | return _singleton 27 | -------------------------------------------------------------------------------- /app/utils/dom_utils.py: -------------------------------------------------------------------------------- 1 | class DomUtils: 2 | 3 | @staticmethod 4 | def tag_value(tag_item, tag_name, attname="", default=None): 5 | """ 6 | 解析XML标签值 7 | """ 8 | tagNames = tag_item.getElementsByTagName(tag_name) 9 | if tagNames: 10 | if attname: 11 | attvalue = tagNames[0].getAttribute(attname) 12 | if attvalue: 13 | return attvalue 14 | else: 15 | firstChild = tagNames[0].firstChild 16 | if firstChild: 17 | return firstChild.data 18 | return default 19 | 20 | @staticmethod 21 | def add_node(doc, parent, name, value=None): 22 | """ 23 | 添加一个DOM节点 24 | """ 25 | node = doc.createElement(name) 26 | parent.appendChild(node) 27 | if value is not None: 28 | text = doc.createTextNode(str(value)) 29 | node.appendChild(text) 30 | return node 31 | -------------------------------------------------------------------------------- /app/utils/episode_format.py: -------------------------------------------------------------------------------- 1 | import re 2 | import parse 3 | from config import SPLIT_CHARS 4 | 5 | 6 | class EpisodeFormat(object): 7 | _key = "" 8 | 9 | def __init__(self, eformat, details: str = None, offset=None, key="ep"): 10 | self._format = eformat 11 | self._start_ep = None 12 | self._end_ep = None 13 | if details: 14 | if re.compile("\\d{1,4}-\\d{1,4}").match(details): 15 | self._start_ep = details 16 | self._end_ep = details 17 | else: 18 | tmp = details.split(",") 19 | if len(tmp) > 1: 20 | self._start_ep = int(tmp[0]) 21 | self._end_ep = int(tmp[0]) if int(tmp[0]) > int(tmp[1]) else int(tmp[1]) 22 | else: 23 | self._start_ep = self._end_ep = int(tmp[0]) 24 | self.__offset = int(offset) if offset else 0 25 | self._key = key 26 | 27 | @property 28 | def format(self): 29 | return self._format 30 | 31 | @property 32 | def start_ep(self): 33 | return self._start_ep 34 | 35 | @property 36 | def end_ep(self): 37 | return self._end_ep 38 | 39 | @property 40 | def offset(self): 41 | return self.__offset 42 | 43 | def match(self, file: str): 44 | if not self._format: 45 | return True 46 | s, e = self.__handle_single(file) 47 | if not s: 48 | return False 49 | if self._start_ep is None: 50 | return True 51 | if self._start_ep <= s <= self._end_ep: 52 | return True 53 | return False 54 | 55 | def split_episode(self, file_name): 56 | # 指定的具体集数,直接返回 57 | if self._start_ep is not None and self._start_ep == self._end_ep: 58 | if isinstance(self._start_ep, str): 59 | s, e = self._start_ep.split("-") 60 | if int(s) == int(e): 61 | return int(s) + self.__offset, None 62 | return int(s) + self.__offset, int(e) + self.__offset 63 | return self._start_ep + self.__offset, None 64 | if not self._format: 65 | return None, None 66 | s, e = self.__handle_single(file_name) 67 | return s + self.__offset if s is not None else None, e + self.__offset if e is not None else None 68 | 69 | def __handle_single(self, file: str): 70 | if not self._format: 71 | return None, None 72 | ret = parse.parse(self._format, file) 73 | if not ret or not ret.__contains__(self._key): 74 | return None, None 75 | episodes = ret.__getitem__(self._key) 76 | if not re.compile(r"^(EP)?(\d{1,4})(-(EP)?(\d{1,4}))?$", re.IGNORECASE).match(episodes): 77 | return None, None 78 | episode_splits = list(filter(lambda x: re.compile(r'[a-zA-Z]*\d{1,4}', re.IGNORECASE).match(x), 79 | re.split(r'%s' % SPLIT_CHARS, episodes))) 80 | if len(episode_splits) == 1: 81 | return int(re.compile(r'[a-zA-Z]*', re.IGNORECASE).sub("", episode_splits[0])), None 82 | else: 83 | return int(re.compile(r'[a-zA-Z]*', re.IGNORECASE).sub("", episode_splits[0])), int( 84 | re.compile(r'[a-zA-Z]*', re.IGNORECASE).sub("", episode_splits[1])) 85 | 86 | -------------------------------------------------------------------------------- /app/utils/exception_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import traceback 3 | 4 | 5 | class ExceptionUtils: 6 | @classmethod 7 | def exception_traceback(cls, e): 8 | print(f"\nException: {str(e)}\nCallstack:\n{traceback.format_exc()}\n") 9 | -------------------------------------------------------------------------------- /app/utils/json_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from enum import Enum 3 | 4 | 5 | class JsonUtils: 6 | 7 | @staticmethod 8 | def json_serializable(obj): 9 | """ 10 | 将普通对象转化为支持json序列化的对象 11 | @param obj: 待转化的对象 12 | @return: 支持json序列化的对象 13 | """ 14 | 15 | def _try(o): 16 | if isinstance(o, Enum): 17 | return o.value 18 | try: 19 | return o.__dict__ 20 | except Exception as err: 21 | print(str(err)) 22 | return str(o) 23 | 24 | return json.loads(json.dumps(obj, default=lambda o: _try(o))) 25 | -------------------------------------------------------------------------------- /app/utils/number_utils.py: -------------------------------------------------------------------------------- 1 | class NumberUtils: 2 | 3 | @staticmethod 4 | def max_ele(a, b): 5 | """ 6 | 返回非空最大值 7 | """ 8 | if not a: 9 | return b 10 | if not b: 11 | return a 12 | return max(int(a), int(b)) 13 | -------------------------------------------------------------------------------- /app/utils/rsstitle_utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from app.utils.exception_utils import ExceptionUtils 4 | 5 | 6 | class RssTitleUtils: 7 | 8 | @staticmethod 9 | def keepfriends_title(title): 10 | """ 11 | 处理pt.keepfrds.com的RSS标题 12 | """ 13 | if not title: 14 | return "" 15 | try: 16 | title_search = re.search(r"\[(.*)]", title, re.IGNORECASE) 17 | if title_search: 18 | if title_search.span()[0] == 0: 19 | title_all = re.findall(r"\[(.*?)]", title, re.IGNORECASE) 20 | if title_all and len(title_all) > 1: 21 | torrent_name = title_all[-1] 22 | torrent_desc = title.replace(f"[{torrent_name}]", "").strip() 23 | title = "%s %s" % (torrent_name, torrent_desc) 24 | else: 25 | torrent_name = title_search.group(1) 26 | torrent_desc = title.replace(title_search.group(), "").strip() 27 | title = "%s %s" % (torrent_name, torrent_desc) 28 | except Exception as err: 29 | ExceptionUtils.exception_traceback(err) 30 | return title 31 | -------------------------------------------------------------------------------- /app/utils/tokens.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from config import SPLIT_CHARS 4 | 5 | 6 | class Tokens: 7 | _text = "" 8 | _index = 0 9 | _tokens = [] 10 | 11 | def __init__(self, text): 12 | self._text = text 13 | self._tokens = [] 14 | self.load_text(text) 15 | 16 | def load_text(self, text): 17 | splited_text = re.split(r'%s' % SPLIT_CHARS, text) 18 | for sub_text in splited_text: 19 | if sub_text: 20 | self._tokens.append(sub_text) 21 | 22 | def cur(self): 23 | if self._index >= len(self._tokens): 24 | return None 25 | else: 26 | token = self._tokens[self._index] 27 | return token 28 | 29 | def get_next(self): 30 | token = self.cur() 31 | if token: 32 | self._index = self._index + 1 33 | return token 34 | 35 | def peek(self): 36 | index = self._index + 1 37 | if index >= len(self._tokens): 38 | return None 39 | else: 40 | return self._tokens[index] 41 | -------------------------------------------------------------------------------- /app/utils/types.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class MediaType(Enum): 5 | TV = '电视剧' 6 | MOVIE = '电影' 7 | ANIME = '动漫' 8 | UNKNOWN = '未知' 9 | 10 | 11 | class DownloaderType(Enum): 12 | QB = 'Qbittorrent' 13 | TR = 'Transmission' 14 | Client115 = '115网盘' 15 | Aria2 = 'Aria2' 16 | PikPak = 'PikPak' 17 | 18 | 19 | class SyncType(Enum): 20 | MAN = "手动整理" 21 | MON = "目录同步" 22 | 23 | 24 | class SearchType(Enum): 25 | WX = "微信" 26 | WEB = "WEB" 27 | DB = "豆瓣" 28 | RSS = "电影/电视剧订阅" 29 | USERRSS = "自定义订阅" 30 | OT = "手动下载" 31 | TG = "Telegram" 32 | API = "第三方API请求" 33 | SLACK = "Slack" 34 | SYNOLOGY = "Synology Chat" 35 | 36 | 37 | class RmtMode(Enum): 38 | LINK = "硬链接" 39 | SOFTLINK = "软链接" 40 | COPY = "复制" 41 | MOVE = "移动" 42 | RCLONECOPY = "Rclone复制" 43 | RCLONE = "Rclone移动" 44 | MINIOCOPY = "Minio复制" 45 | MINIO = "Minio移动" 46 | 47 | 48 | class MatchMode(Enum): 49 | NORMAL = "正常模式" 50 | STRICT = "严格模式" 51 | 52 | 53 | class OsType(Enum): 54 | WINDOWS = "Windows" 55 | LINUX = "Linux" 56 | SYNOLOGY = "Synology" 57 | MACOS = "MacOS" 58 | DOCKER = "Docker" 59 | 60 | 61 | class IndexerType(Enum): 62 | JACKETT = "Jackett" 63 | PROWLARR = "Prowlarr" 64 | BUILTIN = "Indexer" 65 | 66 | 67 | class MediaServerType(Enum): 68 | JELLYFIN = "Jellyfin" 69 | EMBY = "Emby" 70 | PLEX = "Plex" 71 | 72 | 73 | class BrushDeleteType(Enum): 74 | NOTDELETE = "不删除" 75 | SEEDTIME = "做种时间" 76 | RATIO = "分享率" 77 | UPLOADSIZE = "上传量" 78 | DLTIME = "下载耗时" 79 | AVGUPSPEED = "平均上传速度" 80 | IATIME = "未活动时间" 81 | 82 | 83 | # 站点框架 84 | class SiteSchema(Enum): 85 | DiscuzX = "Discuz!" 86 | Gazelle = "Gazelle" 87 | Ipt = "IPTorrents" 88 | NexusPhp = "NexusPhp" 89 | NexusProject = "NexusProject" 90 | NexusRabbit = "NexusRabbit" 91 | SmallHorse = "Small Horse" 92 | Unit3d = "Unit3d" 93 | TorrentLeech = "TorrentLeech" 94 | FileList = "FileList" 95 | TNode = "TNode" 96 | 97 | 98 | MovieTypes = ['MOV', '电影'] 99 | TvTypes = ['TV', '电视剧'] 100 | -------------------------------------------------------------------------------- /build_sites.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import pickle 3 | import ruamel.yaml 4 | from app.utils.path_utils import PathUtils 5 | from config import Config 6 | 7 | 8 | if __name__ == "__main__": 9 | _indexers = [] 10 | _site_path = os.path.join(Config().get_config_path(), "sites") 11 | cfg_files = PathUtils.get_dir_files(in_path=_site_path, exts=[".yml"]) 12 | for cfg_file in cfg_files: 13 | with open(cfg_file, mode='r', encoding='utf-8') as f: 14 | print(cfg_file) 15 | _indexers.append(ruamel.yaml.YAML().load(f)) 16 | with open(os.path.join(Config().get_inner_config_path(), "sites.dat"), 'wb') as f: 17 | pickle.dump(_indexers, f, pickle.HIGHEST_PROTOCOL) 18 | -------------------------------------------------------------------------------- /config/init_userrss_v3.sql: -------------------------------------------------------------------------------- 1 | INSERT OR REPLACE INTO "CONFIG_RSS_PARSER" ("ID", "NAME", "TYPE", "FORMAT", "PARAMS", "NOTE", "SYSDEF") VALUES ('1', '通用', 'XML', '{ 2 | "list": "//channel/item", 3 | "item": { 4 | "title": { 5 | "path": ".//title/text()" 6 | }, 7 | "enclosure": { 8 | "path": ".//enclosure[@type=''application/x-bittorrent'']/@url" 9 | }, 10 | "link": { 11 | "path": ".//link/text()" 12 | }, 13 | "date": { 14 | "path": ".//pubDate/text()" 15 | }, 16 | "description": { 17 | "path": ".//description/text()" 18 | }, 19 | "size": { 20 | "path": ".//link/@length" 21 | } 22 | } 23 | }', '', '', 'Y'); 24 | INSERT OR REPLACE INTO "CONFIG_RSS_PARSER" ("ID", "NAME", "TYPE", "FORMAT", "PARAMS", "NOTE", "SYSDEF") VALUES ('2', '蜜柑计划', 'XML', '{ 25 | "list": "//channel/item", 26 | "item": { 27 | "title": { 28 | "path": ".//title/text()" 29 | }, 30 | "enclosure": { 31 | "path": ".//enclosure[@type=''application/x-bittorrent'']/@url" 32 | }, 33 | "link": { 34 | "path": "link/text()", 35 | "namespaces": "https://mikanani.me/0.1/" 36 | }, 37 | "date": { 38 | "path": "pubDate/text()", 39 | "namespaces": "https://mikanani.me/0.1/" 40 | }, 41 | "description": { 42 | "path": ".//description/text()" 43 | }, 44 | "size": { 45 | "path": ".//enclosure[@type=''application/x-bittorrent'']/@length" 46 | } 47 | } 48 | }', '', '', 'Y'); 49 | INSERT OR REPLACE INTO "CONFIG_RSS_PARSER" ("ID", "NAME", "TYPE", "FORMAT", "PARAMS", "NOTE", "SYSDEF") VALUES ('3', 'TMDB电影片单', 'JSON', '{ 50 | "list": "$.items", 51 | "item": { 52 | "title": { 53 | "path": "title" 54 | }, 55 | "year": { 56 | "path": "release_date" 57 | }, 58 | "type": { 59 | "value": "movie" 60 | } 61 | } 62 | }', 'api_key={TMDBKEY}&language=zh-CN', '', 'Y'); 63 | INSERT OR REPLACE INTO "CONFIG_RSS_PARSER" ("ID", "NAME", "TYPE", "FORMAT", "PARAMS", "NOTE", "SYSDEF") VALUES ('4', 'TMDB电视剧片单', 'JSON', '{ 64 | "list": "$.items", 65 | "item": { 66 | "title": { 67 | "path": "name" 68 | }, 69 | "year": { 70 | "path": "first_air_date" 71 | }, 72 | "type": { 73 | "value": "tv" 74 | } 75 | } 76 | }', 'api_key={TMDBKEY}&language=zh-CN', '', 'Y'); 77 | INSERT OR REPLACE INTO "CONFIG_RSS_PARSER" ("ID", "NAME", "TYPE", "FORMAT", "PARAMS", "NOTE", "SYSDEF") VALUES ('5', 'Nyaa', 'XML', '{ 78 | "list": "//channel/item", 79 | "item": { 80 | "title": { 81 | "path": ".//title/text()" 82 | }, 83 | "enclosure": { 84 | "path": ".//link/text()" 85 | }, 86 | "link": { 87 | "path": ".//guid/text()" 88 | }, 89 | "date": { 90 | "path": ".//pubDate/text()" 91 | }, 92 | "description": { 93 | "path": ".//description/text()" 94 | }, 95 | "size": { 96 | "path": "size/text()", 97 | "namespaces": "https://nyaa.si/xmlns/nyaa" 98 | } 99 | } 100 | }', '', '', 'Y'); -------------------------------------------------------------------------------- /config/reset_db_version.sql: -------------------------------------------------------------------------------- 1 | delete from alembic_version where 1 -------------------------------------------------------------------------------- /config/sites.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/config/sites.dat -------------------------------------------------------------------------------- /config/update_subscribe.sql: -------------------------------------------------------------------------------- 1 | UPDATE RSS_MOVIES SET DOWNLOAD_SETTING = null WHERE DOWNLOAD_SETTING = -1; 2 | UPDATE RSS_TVS SET DOWNLOAD_SETTING = null WHERE DOWNLOAD_SETTING = -1; 3 | -------------------------------------------------------------------------------- /config/update_userpris.sql: -------------------------------------------------------------------------------- 1 | UPDATE main.CONFIG_USERS SET PRIS = replace(PRIS, '推荐', '探索') WHERE 1 -------------------------------------------------------------------------------- /config/update_userrss.sql: -------------------------------------------------------------------------------- 1 | UPDATE CONFIG_USER_RSS SET PROCESS_COUNT = '0' WHERE PROCESS_COUNT is null -------------------------------------------------------------------------------- /db_scripts/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /db_scripts/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config 4 | from sqlalchemy import pool 5 | 6 | from alembic import context 7 | 8 | from app.db.models import Base 9 | # this is the Alembic Config object, which provides 10 | # access to the values within the .ini file in use. 11 | config = context.config 12 | 13 | # Interpret the config file for Python logging. 14 | # This line sets up loggers basically. 15 | if config.config_file_name is not None: 16 | fileConfig(config.config_file_name) 17 | 18 | # add your model's MetaData object here 19 | # for 'autogenerate' support 20 | # from myapp import mymodel 21 | # target_metadata = mymodel.Base.metadata 22 | target_metadata = Base.metadata 23 | 24 | # other values from the config, defined by the needs of env.py, 25 | # can be acquired: 26 | # my_important_option = config.get_main_option("my_important_option") 27 | # ... etc. 28 | 29 | 30 | def run_migrations_offline() -> None: 31 | """Run migrations in 'offline' mode. 32 | 33 | This configures the context with just a URL 34 | and not an Engine, though an Engine is acceptable 35 | here as well. By skipping the Engine creation 36 | we don't even need a DBAPI to be available. 37 | 38 | Calls to context.execute() here emit the given string to the 39 | script output. 40 | 41 | """ 42 | url = config.get_main_option("sqlalchemy.url") 43 | context.configure( 44 | url=url, 45 | target_metadata=target_metadata, 46 | literal_binds=True, 47 | dialect_opts={"paramstyle": "named"}, 48 | render_as_batch=True 49 | ) 50 | 51 | with context.begin_transaction(): 52 | context.run_migrations() 53 | 54 | 55 | def run_migrations_online() -> None: 56 | """Run migrations in 'online' mode. 57 | 58 | In this scenario we need to create an Engine 59 | and associate a connection with the context. 60 | 61 | """ 62 | connectable = engine_from_config( 63 | config.get_section(config.config_ini_section), 64 | prefix="sqlalchemy.", 65 | poolclass=pool.NullPool, 66 | ) 67 | 68 | with connectable.connect() as connection: 69 | context.configure( 70 | connection=connection, target_metadata=target_metadata 71 | ) 72 | 73 | with context.begin_transaction(): 74 | context.run_migrations() 75 | 76 | 77 | if context.is_offline_mode(): 78 | run_migrations_offline() 79 | else: 80 | run_migrations_online() 81 | -------------------------------------------------------------------------------- /db_scripts/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade() -> None: 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade() -> None: 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /dbscript_gen.py: -------------------------------------------------------------------------------- 1 | import os 2 | from config import Config 3 | from alembic.config import Config as AlembicConfig 4 | from alembic.command import revision as alembic_revision 5 | 6 | db_version = input("请输入版本号:") 7 | db_location = os.path.join(Config().get_config_path(), 'user.db').replace('\\', '/') 8 | script_location = os.path.join(os.path.dirname(__file__), 'db_scripts').replace('\\', '/') 9 | alembic_cfg = AlembicConfig() 10 | alembic_cfg.set_main_option('script_location', script_location) 11 | alembic_cfg.set_main_option('sqlalchemy.url', f"sqlite:///{db_location}") 12 | alembic_revision(alembic_cfg, db_version, True) 13 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | RUN apk add --no-cache libffi-dev \ 3 | && apk add --no-cache $(echo $(wget --no-check-certificate -qO- https://raw.githubusercontent.com/jxxghp/nas-tools/master/package_list.txt)) \ 4 | && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \ 5 | && echo "${TZ}" > /etc/timezone \ 6 | && ln -sf /usr/bin/python3 /usr/bin/python \ 7 | && curl https://rclone.org/install.sh | bash \ 8 | && if [ "$(uname -m)" = "x86_64" ]; then ARCH=amd64; elif [ "$(uname -m)" = "aarch64" ]; then ARCH=arm64; fi \ 9 | && curl https://dl.min.io/client/mc/release/linux-${ARCH}/mc --create-dirs -o /usr/bin/mc \ 10 | && chmod +x /usr/bin/mc \ 11 | && pip install --upgrade pip setuptools wheel \ 12 | && pip install cython \ 13 | && pip install -r https://raw.githubusercontent.com/jxxghp/nas-tools/master/requirements.txt \ 14 | && apk del libffi-dev \ 15 | && npm install pm2 -g \ 16 | && rm -rf /tmp/* /root/.cache /var/cache/apk/* 17 | ENV LANG="C.UTF-8" \ 18 | TZ="Asia/Shanghai" \ 19 | NASTOOL_CONFIG="/config/config.yaml" \ 20 | NASTOOL_AUTO_UPDATE=true \ 21 | NASTOOL_CN_UPDATE=true \ 22 | NASTOOL_VERSION=master \ 23 | PS1="\u@\h:\w \$ " \ 24 | REPO_URL="https://github.com/jxxghp/nas-tools.git" \ 25 | PYPI_MIRROR="https://pypi.tuna.tsinghua.edu.cn/simple" \ 26 | ALPINE_MIRROR="mirrors.ustc.edu.cn" \ 27 | PUID=0 \ 28 | PGID=0 \ 29 | UMASK=000 \ 30 | WORKDIR="/nas-tools" 31 | WORKDIR ${WORKDIR} 32 | RUN python_ver=$(python3 -V | awk '{print $2}') \ 33 | && echo "${WORKDIR}/" > /usr/lib/python${python_ver%.*}/site-packages/nas-tools.pth \ 34 | && echo 'fs.inotify.max_user_watches=524288' >> /etc/sysctl.conf \ 35 | && echo 'fs.inotify.max_user_instances=524288' >> /etc/sysctl.conf \ 36 | && git config --global pull.ff only \ 37 | && git clone -b master ${REPO_URL} ${WORKDIR} --depth=1 --recurse-submodule \ 38 | && git config --global --add safe.directory ${WORKDIR} 39 | EXPOSE 3000 40 | VOLUME ["/config"] 41 | ENTRYPOINT ["/nas-tools/docker/entrypoint.sh"] -------------------------------------------------------------------------------- /docker/Dockerfile.beta: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | RUN apk add --no-cache libffi-dev \ 3 | && apk add --no-cache $(echo $(wget --no-check-certificate -qO- https://raw.githubusercontent.com/jxxghp/nas-tools/dev/package_list.txt)) \ 4 | && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \ 5 | && echo "${TZ}" > /etc/timezone \ 6 | && ln -sf /usr/bin/python3 /usr/bin/python \ 7 | && curl https://rclone.org/install.sh | bash \ 8 | && if [ "$(uname -m)" = "x86_64" ]; then ARCH=amd64; elif [ "$(uname -m)" = "aarch64" ]; then ARCH=arm64; fi \ 9 | && curl https://dl.min.io/client/mc/release/linux-${ARCH}/mc --create-dirs -o /usr/bin/mc \ 10 | && chmod +x /usr/bin/mc \ 11 | && pip install --upgrade pip setuptools wheel \ 12 | && pip install cython \ 13 | && pip install -r https://raw.githubusercontent.com/jxxghp/nas-tools/dev/requirements.txt \ 14 | && apk del libffi-dev \ 15 | && npm install pm2 -g \ 16 | && rm -rf /tmp/* /root/.cache /var/cache/apk/* 17 | ENV LANG="C.UTF-8" \ 18 | TZ="Asia/Shanghai" \ 19 | NASTOOL_CONFIG="/config/config.yaml" \ 20 | NASTOOL_AUTO_UPDATE=true \ 21 | NASTOOL_CN_UPDATE=true \ 22 | NASTOOL_VERSION=dev \ 23 | PS1="\u@\h:\w \$ " \ 24 | REPO_URL="https://github.com/jxxghp/nas-tools.git" \ 25 | PYPI_MIRROR="https://pypi.tuna.tsinghua.edu.cn/simple" \ 26 | ALPINE_MIRROR="mirrors.ustc.edu.cn" \ 27 | PUID=0 \ 28 | PGID=0 \ 29 | UMASK=000 \ 30 | WORKDIR="/nas-tools" 31 | WORKDIR ${WORKDIR} 32 | RUN python_ver=$(python3 -V | awk '{print $2}') \ 33 | && echo "${WORKDIR}/" > /usr/lib/python${python_ver%.*}/site-packages/nas-tools.pth \ 34 | && echo 'fs.inotify.max_user_watches=524288' >> /etc/sysctl.conf \ 35 | && echo 'fs.inotify.max_user_instances=524288' >> /etc/sysctl.conf \ 36 | && git config --global pull.ff only \ 37 | && git clone -b dev ${REPO_URL} ${WORKDIR} --depth=1 --recurse-submodule \ 38 | && git config --global --add safe.directory ${WORKDIR} 39 | EXPOSE 3000 40 | VOLUME ["/config"] 41 | ENTRYPOINT ["/nas-tools/docker/entrypoint.sh"] 42 | -------------------------------------------------------------------------------- /docker/Dockerfile.lite: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | RUN apk add --no-cache libffi-dev \ 3 | git \ 4 | gcc \ 5 | musl-dev \ 6 | python3-dev \ 7 | py3-pip \ 8 | libxml2-dev \ 9 | libxslt-dev \ 10 | tzdata \ 11 | su-exec \ 12 | dumb-init \ 13 | npm \ 14 | && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \ 15 | && echo "${TZ}" > /etc/timezone \ 16 | && ln -sf /usr/bin/python3 /usr/bin/python \ 17 | && pip install --upgrade pip setuptools wheel \ 18 | && pip install cython \ 19 | && pip install -r https://raw.githubusercontent.com/jxxghp/nas-tools/master/requirements.txt \ 20 | && npm install pm2 -g \ 21 | && apk del --purge libffi-dev gcc musl-dev libxml2-dev libxslt-dev \ 22 | && pip uninstall -y cython \ 23 | && rm -rf /tmp/* /root/.cache /var/cache/apk/* 24 | ENV LANG="C.UTF-8" \ 25 | TZ="Asia/Shanghai" \ 26 | NASTOOL_CONFIG="/config/config.yaml" \ 27 | NASTOOL_AUTO_UPDATE=false \ 28 | NASTOOL_CN_UPDATE=true \ 29 | NASTOOL_VERSION=lite \ 30 | PS1="\u@\h:\w \$ " \ 31 | REPO_URL="https://github.com/jxxghp/nas-tools.git" \ 32 | PYPI_MIRROR="https://pypi.tuna.tsinghua.edu.cn/simple" \ 33 | ALPINE_MIRROR="mirrors.ustc.edu.cn" \ 34 | PUID=0 \ 35 | PGID=0 \ 36 | UMASK=000 \ 37 | WORKDIR="/nas-tools" 38 | WORKDIR ${WORKDIR} 39 | RUN python_ver=$(python3 -V | awk '{print $2}') \ 40 | && echo "${WORKDIR}/" > /usr/lib/python${python_ver%.*}/site-packages/nas-tools.pth \ 41 | && echo 'fs.inotify.max_user_watches=524288' >> /etc/sysctl.conf \ 42 | && echo 'fs.inotify.max_user_instances=524288' >> /etc/sysctl.conf \ 43 | && git config --global pull.ff only \ 44 | && git clone -b master ${REPO_URL} ${WORKDIR} --depth=1 --recurse-submodule \ 45 | && git config --global --add safe.directory ${WORKDIR} 46 | EXPOSE 3000 47 | VOLUME ["/config"] 48 | ENTRYPOINT ["/nas-tools/docker/entrypoint.sh"] -------------------------------------------------------------------------------- /docker/compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | nas-tools: 4 | image: jxxghp/nas-tools:latest 5 | ports: 6 | - 3000:3000 # 默认的webui控制端口 7 | volumes: 8 | - ./config:/config # 冒号左边请修改为你想保存配置的路径 9 | - /你的媒体目录:/你想设置的容器内能见到的目录 # 媒体目录,多个目录需要分别映射进来,需要满足配置文件说明中的要求 10 | environment: 11 | - PUID=0 # 想切换为哪个用户来运行程序,该用户的uid 12 | - PGID=0 # 想切换为哪个用户来运行程序,该用户的gid 13 | - UMASK=000 # 掩码权限,默认000,可以考虑设置为022 14 | - NASTOOL_AUTO_UPDATE=false # 如需在启动容器时自动升级程程序请设置为true 15 | #- REPO_URL=https://ghproxy.com/https://github.com/jxxghp/nas-tools.git # 当你访问github网络很差时,可以考虑解释本行注释 16 | restart: always 17 | network_mode: bridge 18 | hostname: nas-tools 19 | container_name: nas-tools -------------------------------------------------------------------------------- /docker/readme.md: -------------------------------------------------------------------------------- 1 | ## 特点 2 | 3 | - 基于alpine实现,镜像体积小; 4 | 5 | - 镜像层数少; 6 | 7 | - 支持 amd64/arm64 架构; 8 | 9 | - 重启即可更新程序,如果依赖有变化,会自动尝试重新安装依赖,若依赖自动安装不成功,会提示更新镜像; 10 | 11 | - 可以以非root用户执行任务,降低程序权限和潜在风险; 12 | 13 | - 可以设置文件掩码权限umask。 14 | 15 | - lite 版本不包含浏览器内核及xvfb,不支持浏览器仿真;不支持Rclone/Minio转移方式;不支持复杂依赖变更时的自动安装升级;但是体积更小。 16 | 17 | ## 创建 18 | 19 | **注意** 20 | 21 | - 媒体目录的设置必须符合 [配置说明](https://github.com/jxxghp/nas-tools#%E9%85%8D%E7%BD%AE) 的要求。 22 | 23 | - umask含义详见:http://www.01happy.com/linux-umask-analyze 。 24 | 25 | - 创建后请根据 [配置说明](https://github.com/jxxghp/nas-tools#%E9%85%8D%E7%BD%AE) 及该文件本身的注释,修改`config/config.yaml`,修改好后再重启容器,最后访问`http://:`。 26 | 27 | **docker cli** 28 | 29 | ``` 30 | docker run -d \ 31 | --name nas-tools \ 32 | --hostname nas-tools \ 33 | -p 3000:3000 `# 默认的webui控制端口` \ 34 | -v $(pwd)/config:/config `# 冒号左边请修改为你想在主机上保存配置文件的路径` \ 35 | -v /你的媒体目录:/你想设置的容器内能见到的目录 `# 媒体目录,多个目录需要分别映射进来` \ 36 | -e PUID=0 `# 想切换为哪个用户来运行程序,该用户的uid,详见下方说明` \ 37 | -e PGID=0 `# 想切换为哪个用户来运行程序,该用户的gid,详见下方说明` \ 38 | -e UMASK=000 `# 掩码权限,默认000,可以考虑设置为022` \ 39 | -e NASTOOL_AUTO_UPDATE=false `# 如需在启动容器时自动升级程程序请设置为true` \ 40 | -e NASTOOL_CN_UPDATE=false `# 如果开启了容器启动自动升级程序,并且网络不太友好时,可以设置为true,会使用国内源进行软件更新` \ 41 | jxxghp/nas-tools 42 | ``` 43 | 44 | 如果你访问github的网络不太好,可以考虑在创建容器时增加设置一个环境变量`-e REPO_URL="https://ghproxy.com/https://github.com/jxxghp/nas-tools.git" \`。 45 | 46 | **docker-compose** 47 | 48 | 新建`docker-compose.yaml`文件如下,并以命令`docker-compose up -d`启动。 49 | 50 | ``` 51 | version: "3" 52 | services: 53 | nas-tools: 54 | image: jxxghp/nas-tools:latest 55 | ports: 56 | - 3000:3000 # 默认的webui控制端口 57 | volumes: 58 | - ./config:/config # 冒号左边请修改为你想保存配置的路径 59 | - /你的媒体目录:/你想设置的容器内能见到的目录 # 媒体目录,多个目录需要分别映射进来,需要满足配置文件说明中的要求 60 | environment: 61 | - PUID=0 # 想切换为哪个用户来运行程序,该用户的uid 62 | - PGID=0 # 想切换为哪个用户来运行程序,该用户的gid 63 | - UMASK=000 # 掩码权限,默认000,可以考虑设置为022 64 | - NASTOOL_AUTO_UPDATE=false # 如需在启动容器时自动升级程程序请设置为true 65 | - NASTOOL_CN_UPDATE=false # 如果开启了容器启动自动升级程序,并且网络不太友好时,可以设置为true,会使用国内源进行软件更新 66 | #- REPO_URL=https://ghproxy.com/https://github.com/jxxghp/nas-tools.git # 当你访问github网络很差时,可以考虑解释本行注释 67 | restart: always 68 | network_mode: bridge 69 | hostname: nas-tools 70 | container_name: nas-tools 71 | ``` 72 | 73 | ## 后续如何更新 74 | 75 | - 正常情况下,如果设置了`NASTOOL_AUTO_UPDATE=true`,重启容器即可自动更新nas-tools程序。 76 | 77 | - 设置了`NASTOOL_AUTO_UPDATE=true`时,如果启动时的日志提醒你 "更新失败,继续使用旧的程序来启动...",请再重启一次,如果一直都报此错误,请改善你的网络。 78 | 79 | - 设置了`NASTOOL_AUTO_UPDATE=true`时,如果启动时的日志提醒你 "无法安装依赖,请更新镜像...",则需要删除旧容器,删除旧镜像,重新pull镜像,再重新创建容器。 80 | 81 | ## 关于PUID/PGID的说明 82 | 83 | - 如在使用诸如emby、jellyfin、plex、qbittorrent、transmission、deluge、jackett、sonarr、radarr等等的docker镜像,请保证创建本容器时的PUID/PGID和它们一样。 84 | 85 | - 在docker宿主上,登陆媒体文件所有者的这个用户,然后分别输入`id -u`和`id -g`可获取到uid和gid,分别设置为PUID和PGID即可。 86 | 87 | - `PUID=0` `PGID=0`指root用户,它拥有最高权限,若你的媒体文件的所有者不是root,不建议设置为`PUID=0` `PGID=0`。 88 | 89 | ## 如果要硬连接如何映射 90 | 91 | 参考下图,由imogel@telegram制作。 92 | 93 | ![如何映射](volume.png) 94 | -------------------------------------------------------------------------------- /docker/volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/docker/volume.png -------------------------------------------------------------------------------- /package_list.txt: -------------------------------------------------------------------------------- 1 | git 2 | gcc 3 | musl-dev 4 | python3-dev 5 | py3-pip 6 | libxml2-dev 7 | libxslt-dev 8 | tzdata 9 | su-exec 10 | zip 11 | curl 12 | bash 13 | fuse 14 | xvfb 15 | inotify-tools 16 | chromium-chromedriver 17 | npm 18 | dumb-init 19 | ffmpeg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.8.1 2 | aniso8601==9.0.1 3 | APScheduler==3.9.1 4 | asttokens==2.0.8 5 | async-generator==1.10 6 | attrs==22.1.0 7 | backcall==0.2.0 8 | backports.shutil-get-terminal-size==1.0.0 9 | beautifulsoup4==4.11.1 10 | better-exceptions==0.3.3 11 | bs4==0.0.1 12 | cacheout==0.14.1 13 | certifi==2022.6.15 14 | cffi==1.15.1 15 | charset-normalizer==2.1.1 16 | click==8.1.3 17 | cn2an==0.5.17 18 | colorama==0.4.4 19 | colored==1.3.93 20 | cssselect==1.1.0 21 | DBUtils==3.0.2 22 | dateparser==1.1.4 23 | decorator==5.1.1 24 | executing==1.1.0 25 | Flask==2.1.2 26 | Flask-Login==0.6.2 27 | fast-bencode==1.1.3 28 | flask-compress==1.13 29 | flask-restx==0.5.1 30 | greenlet==1.1.3.post0 31 | h11==0.12.0 32 | humanize==4.4.0 33 | idna==3.3 34 | influxdb==5.3.1 35 | itsdangerous==2.1.2 36 | jedi==0.18.1 37 | Jinja2==3.1.2 38 | jsonpath==0.82 39 | jsonschema==4.16.0 40 | loguru==0.6.0 41 | lxml==4.9.1 42 | Mako==1.2.3 43 | MarkupSafe==2.1.1 44 | matplotlib-inline==0.1.6 45 | msgpack==1.0.4 46 | outcome==1.2.0 47 | parse==1.19.0 48 | parsel==1.6.0 49 | parso==0.8.3 50 | pexpect==4.8.0 51 | pickleshare==0.7.5 52 | pikpakapi==0.1.1 53 | proces==0.1.2 54 | prompt-toolkit==3.0.31 55 | ptyprocess==0.7.0 56 | pure-eval==0.2.2 57 | pycparser==2.21 58 | pycryptodome==3.15.0 59 | Pygments==2.13.0 60 | PyJWT==2.5.0 61 | pymongo==4.2.0 62 | PyMySQL==1.0.2 63 | pyperclip==1.8.2 64 | pypushdeer==0.0.3 65 | pyquery==1.4.3 66 | pyrsistent==0.18.1 67 | PySocks==1.7.1 68 | python-dateutil==2.8.2 69 | python-dotenv==0.20.0 70 | pytz==2022.2.1 71 | pytz-deprecation-shim==0.1.0.post0 72 | PyVirtualDisplay==3.0 73 | redis==3.5.3 74 | redis-py-cluster==2.1.3 75 | regex==2022.9.13 76 | requests==2.28.1 77 | ruamel.yaml==0.17.21 78 | ruamel.yaml.clib==0.2.7 79 | selenium==4.4.3 80 | six==1.16.0 81 | slack-sdk==3.19.5 82 | sniffio==1.2.0 83 | sortedcontainers==2.4.0 84 | soupsieve==2.3.2.post1 85 | SQLAlchemy==1.4.42 86 | stack-data==0.5.1 87 | terminal-layout==2.1.2 88 | tqdm==4.64.0 89 | traitlets==5.4.0 90 | trio==0.21.0 91 | trio-websocket==0.9.2 92 | typing_extensions==4.3.0 93 | tzdata==2022.2 94 | tzlocal==4.2 95 | undetected-chromedriver==3.1.7 96 | urllib3==1.26.12 97 | w3lib==2.0.1 98 | watchdog==2.1.9 99 | wcwidth==0.2.5 100 | webdriver-manager==3.8.5 101 | websockets==10.3 102 | Werkzeug==2.1.2 103 | wsproto==1.2.0 104 | zhconv==1.4.3 105 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/tests/__init__.py -------------------------------------------------------------------------------- /tests/cases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/tests/cases/__init__.py -------------------------------------------------------------------------------- /tests/run.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from tests.test_metainfo import MetaInfoTest 4 | 5 | if __name__ == '__main__': 6 | suite = unittest.TestSuite() 7 | # 测试名称识别 8 | suite.addTest(MetaInfoTest('test_metainfo')) 9 | 10 | # 运行测试 11 | runner = unittest.TextTestRunner() 12 | runner.run(suite) 13 | -------------------------------------------------------------------------------- /tests/test_metainfo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from unittest import TestCase 4 | 5 | from app.media.meta import MetaInfo 6 | from tests.cases.meta_cases import meta_cases 7 | 8 | 9 | class MetaInfoTest(TestCase): 10 | def setUp(self) -> None: 11 | pass 12 | 13 | def tearDown(self) -> None: 14 | pass 15 | 16 | def test_metainfo(self): 17 | for info in meta_cases: 18 | if not info.get("title"): 19 | continue 20 | meta_info = MetaInfo(title=info.get("title"), subtitle=info.get("subtitle")) 21 | target = { 22 | "type": meta_info.type.value, 23 | "cn_name": meta_info.cn_name or "", 24 | "en_name": meta_info.en_name or "", 25 | "year": meta_info.year or "", 26 | "part": meta_info.part or "", 27 | "season": meta_info.get_season_string(), 28 | "episode": meta_info.get_episode_string(), 29 | "restype": meta_info.get_edtion_string(), 30 | "pix": meta_info.resource_pix or "", 31 | "video_codec": meta_info.video_encode or "", 32 | "audio_codec": meta_info.audio_encode or "" 33 | } 34 | self.assertEqual(target, info.get("target")) 35 | -------------------------------------------------------------------------------- /third_party.txt: -------------------------------------------------------------------------------- 1 | feapder 2 | qbittorrent-api 3 | anitopy 4 | plexapi 5 | transmission-rpc 6 | slack_bolt -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | APP_VERSION = 'v2.9.1' 2 | -------------------------------------------------------------------------------- /web/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/__init__.py -------------------------------------------------------------------------------- /web/backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/backend/__init__.py -------------------------------------------------------------------------------- /web/backend/user.py: -------------------------------------------------------------------------------- 1 | from flask_login import UserMixin 2 | from werkzeug.security import check_password_hash 3 | 4 | from app.helper import DbHelper 5 | from config import Config 6 | 7 | 8 | class User(UserMixin): 9 | """ 10 | 用户 11 | """ 12 | dbhelper = None 13 | admin_users = [] 14 | 15 | def __init__(self, user=None): 16 | self.dbhelper = DbHelper() 17 | if user: 18 | self.id = user.get('id') 19 | self.username = user.get('name') 20 | self.password_hash = user.get('password') 21 | self.pris = user.get('pris') 22 | self.admin_users = [{ 23 | "id": 0, 24 | "name": Config().get_config('app').get('login_user'), 25 | "password": Config().get_config('app').get('login_password')[6:], 26 | "pris": "我的媒体库,资源搜索,探索,站点管理,订阅管理,下载管理,媒体整理,服务,系统设置" 27 | }] 28 | 29 | def verify_password(self, password): 30 | """ 31 | 验证密码 32 | """ 33 | if self.password_hash is None: 34 | return False 35 | return check_password_hash(self.password_hash, password) 36 | 37 | def get_id(self): 38 | """ 39 | 获取用户ID 40 | """ 41 | return self.id 42 | 43 | def get(self, user_id): 44 | """ 45 | 根据用户ID获取用户实体,为 login_user 方法提供支持 46 | """ 47 | if user_id is None: 48 | return None 49 | for user in self.admin_users: 50 | if user.get('id') == user_id: 51 | return User(user) 52 | for user in self.dbhelper.get_users(): 53 | if not user: 54 | continue 55 | if user.ID == user_id: 56 | return User({"id": user.ID, "name": user.NAME, "password": user.PASSWORD, "pris": user.PRIS}) 57 | return None 58 | 59 | def get_user(self, user_name): 60 | """ 61 | 根据用户名获取用户对像 62 | """ 63 | for user in self.admin_users: 64 | if user.get("name") == user_name: 65 | return User(user) 66 | for user in self.dbhelper.get_users(): 67 | if user.NAME == user_name: 68 | return User({"id": user.ID, "name": user.NAME, "password": user.PASSWORD, "pris": user.PRIS}) 69 | return None 70 | -------------------------------------------------------------------------------- /web/backend/wallpaper.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import datetime 3 | from functools import lru_cache 4 | 5 | from app.media import Media 6 | from app.utils import RequestUtils, ExceptionUtils 7 | from config import Config 8 | 9 | 10 | @lru_cache(maxsize=1) 11 | def get_login_wallpaper(today=datetime.datetime.strftime(datetime.datetime.now(), '%Y%m%d')): 12 | """ 13 | 获取Base64编码的壁纸图片 14 | """ 15 | wallpaper = Config().get_config('app').get('wallpaper') 16 | tmdbkey = Config().get_config('app').get('rmt_tmdbkey') 17 | if (not wallpaper or wallpaper == "themoviedb") and tmdbkey: 18 | img_url = __get_themoviedb_wallpaper(today) 19 | else: 20 | img_url = __get_bing_wallpaper(today) 21 | if img_url: 22 | res = RequestUtils().get_res(img_url) 23 | if res and res.status_code == 200: 24 | return base64.b64encode(res.content).decode() 25 | return "" 26 | 27 | 28 | def __get_themoviedb_wallpaper(today): 29 | """ 30 | 获取TheMovieDb的随机背景图 31 | """ 32 | return Media().get_random_discover_backdrop() 33 | 34 | 35 | def __get_bing_wallpaper(today): 36 | """ 37 | 获取Bing每日壁纸 38 | """ 39 | url = "https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&today=%s" % today 40 | try: 41 | resp = RequestUtils(timeout=5).get_res(url) 42 | except Exception as err: 43 | ExceptionUtils.exception_traceback(err) 44 | return "" 45 | if resp and resp.status_code == 200: 46 | if resp.json(): 47 | for image in resp.json().get('images') or []: 48 | return f"https://cn.bing.com{image.get('url')}" 49 | return "" 50 | -------------------------------------------------------------------------------- /web/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /web/security.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from functools import wraps 3 | 4 | import jwt 5 | from flask import request 6 | 7 | from app.utils import TokenCache 8 | from config import Config 9 | 10 | 11 | def require_auth(func): 12 | """ 13 | API安全认证 14 | """ 15 | 16 | @wraps(func) 17 | def wrapper(*args, **kwargs): 18 | auth = request.headers.get("Authorization") 19 | if auth: 20 | auth = str(auth).split()[-1] 21 | if auth == Config().get_config("security").get("api_key"): 22 | return func(*args, **kwargs) 23 | return { 24 | "code": 401, 25 | "success": False, 26 | "message": "安全认证未通过,请检查ApiKey" 27 | } 28 | 29 | return wrapper 30 | 31 | 32 | def generate_access_token(username: str, algorithm: str = 'HS256', exp: float = 2): 33 | """ 34 | 生成access_token 35 | :param username: 用户名(自定义部分) 36 | :param algorithm: 加密算法 37 | :param exp: 过期时间,默认2小时 38 | :return:token 39 | """ 40 | 41 | now = datetime.datetime.utcnow() 42 | exp_datetime = now + datetime.timedelta(hours=exp) 43 | access_payload = { 44 | 'exp': exp_datetime, 45 | 'iat': now, 46 | 'username': username 47 | } 48 | access_token = jwt.encode(access_payload, 49 | Config().get_config("security").get("api_key"), 50 | algorithm=algorithm) 51 | return access_token 52 | 53 | 54 | def __decode_auth_token(token: str, algorithms='HS256'): 55 | """ 56 | 解密token 57 | :param token:token字符串 58 | :return: 是否有效,playload 59 | """ 60 | key = Config().get_config("security").get("api_key") 61 | try: 62 | payload = jwt.decode(token, 63 | key=key, 64 | algorithms=algorithms) 65 | except jwt.ExpiredSignatureError: 66 | return False, jwt.decode(token, 67 | key=key, 68 | algorithms=algorithms, 69 | options={'verify_exp': False}) 70 | except (jwt.DecodeError, jwt.InvalidTokenError, jwt.ImmatureSignatureError): 71 | return False, {} 72 | else: 73 | return True, payload 74 | 75 | 76 | def identify(auth_header: str): 77 | """ 78 | 用户鉴权,返回是否有效、用户名 79 | """ 80 | flag = False 81 | if auth_header: 82 | flag, payload = __decode_auth_token(auth_header) 83 | if payload: 84 | return flag, payload.get("username") or "" 85 | return flag, "" 86 | 87 | 88 | def login_required(func): 89 | """ 90 | 登录保护,验证用户是否登录 91 | :param func: 92 | :return: 93 | """ 94 | 95 | @wraps(func) 96 | def wrapper(*args, **kwargs): 97 | 98 | def auth_failed(): 99 | return { 100 | "code": 403, 101 | "success": False, 102 | "message": "安全认证未通过,请检查Token" 103 | } 104 | 105 | token = request.headers.get("Authorization", default=None) 106 | if not token: 107 | return auth_failed() 108 | latest_token = TokenCache.get(token) 109 | if not latest_token: 110 | return auth_failed() 111 | flag, username = identify(latest_token) 112 | if not username: 113 | return auth_failed() 114 | if not flag and username: 115 | TokenCache.set(token, generate_access_token(username)) 116 | return func(*args, **kwargs) 117 | 118 | return wrapper 119 | -------------------------------------------------------------------------------- /web/static/components/card/index.js: -------------------------------------------------------------------------------- 1 | export * from "./normal/index.js"; 2 | export * from "./person/index.js"; -------------------------------------------------------------------------------- /web/static/components/card/normal/placeholder.js: -------------------------------------------------------------------------------- 1 | import { html } from "../../utility/lit-core.min.js"; 2 | import { CustomElement } from "../../utility/utility.js"; 3 | 4 | export class NormalCardPlaceholder extends CustomElement { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | static render_placeholder() { 10 | return html` 11 |
12 |
13 |
14 | `; 15 | } 16 | 17 | render() { 18 | return html` 19 |
20 | ${NormalCardPlaceholder.render_placeholder()} 21 |
22 | `; 23 | } 24 | } 25 | 26 | window.customElements.define("normal-card-placeholder", NormalCardPlaceholder); -------------------------------------------------------------------------------- /web/static/components/card/normal/state.js: -------------------------------------------------------------------------------- 1 | import { LitState } from "../../utility/lit-state.js" 2 | 3 | class CardState extends LitState { 4 | static get stateVars() { 5 | return { 6 | more_id: undefined 7 | }; 8 | } 9 | } 10 | 11 | export const cardState = new CardState(); -------------------------------------------------------------------------------- /web/static/components/card/person/index.js: -------------------------------------------------------------------------------- 1 | import { html } from "../../utility/lit-core.min.js"; 2 | import { CustomElement, Golbal } from "../../utility/utility.js"; 3 | 4 | export class PersonCard extends CustomElement { 5 | 6 | static properties = { 7 | person_id: { attribute: "person-id" }, 8 | person_image: { attribute: "person-image" }, 9 | person_name: { attribute: "person-name" }, 10 | person_role: { attribute: "person-role" }, 11 | lazy: {}, 12 | }; 13 | 14 | constructor() { 15 | super(); 16 | this.lazy = "0"; 17 | } 18 | 19 | render() { 20 | return html` 21 |
22 |
23 |
24 | 31 |
32 |

34 | ${this.person_name} 35 |

36 |
38 | ${this.person_role} 39 |
40 |
41 |
42 | `; 43 | } 44 | 45 | } 46 | 47 | window.customElements.define("person-card", PersonCard); -------------------------------------------------------------------------------- /web/static/components/custom/img/index.js: -------------------------------------------------------------------------------- 1 | import { html, nothing } from "../../utility/lit-core.min.js"; 2 | import { CustomElement, Golbal } from "../../utility/utility.js"; 3 | 4 | export class CustomImg extends CustomElement { 5 | 6 | static properties = { 7 | img_src: { attribute: "img-src" }, 8 | img_noimage: { attribute: "img-noimage" }, 9 | img_class: { attribute: "img-class" }, 10 | img_style: { attribute: "img-style" }, 11 | img_ratio: { attribute: "img-ratio" }, 12 | div_style: { attribute: "div-style" }, 13 | img_placeholder: { attribute: "img-placeholder" }, 14 | img_error: { attribute: "img-error" }, 15 | img_src_list: { type: Array }, 16 | lazy: {}, 17 | _placeholder: { state: true }, 18 | _timeout_update_img: { state: true }, 19 | }; 20 | 21 | constructor() { 22 | super(); 23 | this.img_noimage = Golbal.noImage; 24 | this.lazy = "0"; 25 | this.img_placeholder = "1"; 26 | this.img_error = "1"; 27 | this.img_src_list = []; 28 | this._timeout_update_img = 0; 29 | this._placeholder = true; 30 | } 31 | 32 | willUpdate(changedProperties) { 33 | if (changedProperties.has("img_src")) { 34 | this._placeholder = true; 35 | } 36 | if (changedProperties.has("img_src_list")) { 37 | this._timeout_update_img = 0; 38 | this._update_img(); 39 | } 40 | } 41 | 42 | firstUpdated() { 43 | this._query_img = this.querySelector("img"); 44 | } 45 | 46 | _update_img() { 47 | if (this.img_src_list) { 48 | if (this.img_src_list.length > 1) { 49 | this._query_img.classList.remove("lit-custom-img-carousel-show"); 50 | setTimeout(() => { 51 | this.img_src = this.img_src_list[this._timeout_update_img]; 52 | this._timeout_update_img ++; 53 | if (this._timeout_update_img >= this.img_src_list.length) { 54 | this._timeout_update_img = 0; 55 | } 56 | }, 1000); 57 | } else if (this.img_src_list.length == 1) { 58 | this.img_src = this.img_src_list[0]; 59 | } 60 | } 61 | } 62 | 63 | render() { 64 | return html` 65 | 74 |
76 |
77 | { if (this.lazy != "1" && this.img_error == "1") { this.img_src = this.img_noimage } }} 82 | @load=${() => { 83 | this._placeholder = false; 84 | // 图像渐入 85 | if (this.img_src_list.length > 0) { 86 | this._query_img.classList.add("lit-custom-img-carousel"); 87 | setTimeout(() => { 88 | this._query_img.classList.add("lit-custom-img-carousel-show"); 89 | setTimeout(() => { 90 | this._update_img(); 91 | }, 7000); 92 | }, 100); 93 | } 94 | }}/> 95 |
96 | `; 97 | } 98 | 99 | } 100 | 101 | window.customElements.define("custom-img", CustomImg); -------------------------------------------------------------------------------- /web/static/components/custom/index.js: -------------------------------------------------------------------------------- 1 | export * from "./img/index.js"; 2 | export * from "./slide/index.js"; -------------------------------------------------------------------------------- /web/static/components/index.js: -------------------------------------------------------------------------------- 1 | // 导入所有组件 2 | const body_div = document.createElement("div"); 3 | [ 4 | "custom/chips/index.html", 5 | ] 6 | .forEach((name) => { 7 | const my_wc = document.createElement("div"); 8 | $(my_wc).load("../static/components/" + name); 9 | body_div.appendChild(my_wc); 10 | }) 11 | document.body.appendChild(body_div); -------------------------------------------------------------------------------- /web/static/components/layout/index.js: -------------------------------------------------------------------------------- 1 | export * from "./navbar/index.js"; 2 | export * from "./searchbar/index.js"; -------------------------------------------------------------------------------- /web/static/components/layout/navbar/button.js: -------------------------------------------------------------------------------- 1 | import { html } from "../../utility/lit-core.min.js"; 2 | import { CustomElement } from "../../utility/utility.js"; 3 | 4 | 5 | export class LayoutNavbarButton extends CustomElement { 6 | render() { 7 | return html` 8 | 11 | `; 12 | } 13 | } 14 | 15 | 16 | window.customElements.define("layout-navbar-button", LayoutNavbarButton); -------------------------------------------------------------------------------- /web/static/components/lit-index.js: -------------------------------------------------------------------------------- 1 | export * from "./custom/index.js"; 2 | export * from "./card/index.js"; 3 | export * from "./page/index.js"; 4 | export * from "./layout/index.js"; -------------------------------------------------------------------------------- /web/static/components/page/index.js: -------------------------------------------------------------------------------- 1 | export * from "./discovery/index.js"; 2 | export * from "./mediainfo/index.js"; 3 | export * from "./person/index.js"; -------------------------------------------------------------------------------- /web/static/components/page/person/index.js: -------------------------------------------------------------------------------- 1 | import { html } from "../../utility/lit-core.min.js"; 2 | import { CustomElement, Golbal } from "../../utility/utility.js"; 3 | 4 | export class PagePerson extends CustomElement { 5 | static properties = { 6 | page_title: { attribute: "page-title" }, 7 | page_subtitle: { attribute: "page-subtitle"}, 8 | media_type: { attribute: "media-type" }, 9 | tmdbid: { attribute: "media-tmdbid" }, 10 | person_list: { type: Array }, 11 | }; 12 | 13 | constructor() { 14 | super(); 15 | this.person_list = []; 16 | } 17 | 18 | // 仅执行一次 界面首次刷新后 19 | firstUpdated() { 20 | Golbal.get_cache_or_ajax("media_person", this.media_type, { "tmdbid": this.tmdbid, "type": this.media_type}, 21 | (ret) => { 22 | if (ret.code === 0) { 23 | this.person_list = ret.data; 24 | } 25 | } 26 | ); 27 | } 28 | 29 | render() { 30 | return html` 31 |
32 | 40 |
41 |
42 |
43 |
44 | ${this.person_list.length != 0 45 | ? this.person_list.map((item, index) => ( html` 46 | { 52 | navmenu("recommend?type="+this.media_type+"&subtype=person&personid="+item.id+"&title=参演作品&subtitle="+item.name) 53 | }} 54 | > 55 | ` ) ) 56 | : Array(20).fill(html``) 57 | } 58 |
59 |
60 |
61 | `; 62 | } 63 | 64 | } 65 | 66 | 67 | window.customElements.define("page-person", PagePerson); -------------------------------------------------------------------------------- /web/static/css/nprogress.css: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: none; 4 | } 5 | 6 | #nprogress .bar { 7 | background: var(--tblr-primary) !important; 8 | position: fixed; 9 | z-index: 1031; 10 | top: calc(env(safe-area-inset-top) + var(--safe-area-inset-top)); 11 | left: 0; 12 | width: 100%; 13 | height: 2px; 14 | } 15 | 16 | /* Fancy blur effect */ 17 | #nprogress .peg { 18 | display: block; 19 | position: absolute; 20 | right: 0; 21 | width: 100px; 22 | height: 100%; 23 | box-shadow: 0 0 10px var(--tblr-primary), 0 0 5px var(--tblr-primary); 24 | opacity: 1.0; 25 | 26 | -webkit-transform: rotate(0deg) translate(0px, -1px); 27 | -ms-transform: rotate(0deg) translate(0px, -1px); 28 | transform: rotate(0deg) translate(0px, -1px); 29 | } 30 | 31 | /* Remove these to get rid of the spinner */ 32 | #nprogress .spinner { 33 | display: block; 34 | position: fixed; 35 | z-index: 1031; 36 | top: 15px; 37 | right: 15px; 38 | } 39 | 40 | #nprogress .spinner-icon { 41 | width: 18px; 42 | height: 18px; 43 | box-sizing: border-box; 44 | 45 | border: solid 2px transparent; 46 | border-top-color: #29d; 47 | border-left-color: #29d; 48 | border-radius: 50%; 49 | 50 | -webkit-animation: nprogress-spinner 400ms linear infinite; 51 | animation: nprogress-spinner 400ms linear infinite; 52 | } 53 | 54 | .nprogress-custom-parent { 55 | overflow: hidden; 56 | position: relative; 57 | } 58 | 59 | .nprogress-custom-parent #nprogress .spinner, 60 | .nprogress-custom-parent #nprogress .bar { 61 | position: absolute; 62 | } 63 | 64 | @-webkit-keyframes nprogress-spinner { 65 | 0% { -webkit-transform: rotate(0deg); } 66 | 100% { -webkit-transform: rotate(360deg); } 67 | } 68 | @keyframes nprogress-spinner { 69 | 0% { transform: rotate(0deg); } 70 | 100% { transform: rotate(360deg); } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /web/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/favicon.ico -------------------------------------------------------------------------------- /web/static/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /web/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /web/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /web/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /web/static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /web/static/img/115.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/115.jpg -------------------------------------------------------------------------------- /web/static/img/aria2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/aria2.png -------------------------------------------------------------------------------- /web/static/img/bark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/bark.webp -------------------------------------------------------------------------------- /web/static/img/chanify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/chanify.png -------------------------------------------------------------------------------- /web/static/img/chinesesubfinder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/chinesesubfinder.png -------------------------------------------------------------------------------- /web/static/img/emby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/emby.png -------------------------------------------------------------------------------- /web/static/img/filetree/application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/application.png -------------------------------------------------------------------------------- /web/static/img/filetree/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/code.png -------------------------------------------------------------------------------- /web/static/img/filetree/css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/css.png -------------------------------------------------------------------------------- /web/static/img/filetree/db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/db.png -------------------------------------------------------------------------------- /web/static/img/filetree/directory-lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/directory-lock.png -------------------------------------------------------------------------------- /web/static/img/filetree/directory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/directory.png -------------------------------------------------------------------------------- /web/static/img/filetree/doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/doc.png -------------------------------------------------------------------------------- /web/static/img/filetree/file-lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/file-lock.png -------------------------------------------------------------------------------- /web/static/img/filetree/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/file.png -------------------------------------------------------------------------------- /web/static/img/filetree/film.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/film.png -------------------------------------------------------------------------------- /web/static/img/filetree/flash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/flash.png -------------------------------------------------------------------------------- /web/static/img/filetree/folder_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/folder_open.png -------------------------------------------------------------------------------- /web/static/img/filetree/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/html.png -------------------------------------------------------------------------------- /web/static/img/filetree/java.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/java.png -------------------------------------------------------------------------------- /web/static/img/filetree/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/linux.png -------------------------------------------------------------------------------- /web/static/img/filetree/music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/music.png -------------------------------------------------------------------------------- /web/static/img/filetree/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/pdf.png -------------------------------------------------------------------------------- /web/static/img/filetree/php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/php.png -------------------------------------------------------------------------------- /web/static/img/filetree/picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/picture.png -------------------------------------------------------------------------------- /web/static/img/filetree/ppt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/ppt.png -------------------------------------------------------------------------------- /web/static/img/filetree/psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/psd.png -------------------------------------------------------------------------------- /web/static/img/filetree/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/ruby.png -------------------------------------------------------------------------------- /web/static/img/filetree/script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/script.png -------------------------------------------------------------------------------- /web/static/img/filetree/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/spinner.gif -------------------------------------------------------------------------------- /web/static/img/filetree/txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/txt.png -------------------------------------------------------------------------------- /web/static/img/filetree/xls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/xls.png -------------------------------------------------------------------------------- /web/static/img/filetree/zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/filetree/zip.png -------------------------------------------------------------------------------- /web/static/img/gotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/gotify.png -------------------------------------------------------------------------------- /web/static/img/icon-imdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/icon-imdb.png -------------------------------------------------------------------------------- /web/static/img/icons/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/icons/1024.png -------------------------------------------------------------------------------- /web/static/img/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/icons/128.png -------------------------------------------------------------------------------- /web/static/img/icons/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/icons/144.png -------------------------------------------------------------------------------- /web/static/img/icons/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/icons/152.png -------------------------------------------------------------------------------- /web/static/img/icons/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/icons/167.png -------------------------------------------------------------------------------- /web/static/img/icons/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/icons/172.png -------------------------------------------------------------------------------- /web/static/img/icons/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/icons/180.png -------------------------------------------------------------------------------- /web/static/img/icons/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/icons/196.png -------------------------------------------------------------------------------- /web/static/img/icons/196_ALT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/icons/196_ALT.png -------------------------------------------------------------------------------- /web/static/img/icons/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/icons/216.png -------------------------------------------------------------------------------- /web/static/img/icons/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/icons/256.png -------------------------------------------------------------------------------- /web/static/img/icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/icons/512.png -------------------------------------------------------------------------------- /web/static/img/icons/512_ALT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/icons/512_ALT.png -------------------------------------------------------------------------------- /web/static/img/indexer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/indexer.jpg -------------------------------------------------------------------------------- /web/static/img/indexer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/indexer.png -------------------------------------------------------------------------------- /web/static/img/iyuu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/iyuu.png -------------------------------------------------------------------------------- /web/static/img/jackett.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/jackett.png -------------------------------------------------------------------------------- /web/static/img/jellyfin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/jellyfin.jpg -------------------------------------------------------------------------------- /web/static/img/jellyfin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/jellyfin.png -------------------------------------------------------------------------------- /web/static/img/logo-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/logo-16x16.png -------------------------------------------------------------------------------- /web/static/img/logo-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/logo-32x32.png -------------------------------------------------------------------------------- /web/static/img/logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/logo-black.png -------------------------------------------------------------------------------- /web/static/img/logo-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/logo-blue.png -------------------------------------------------------------------------------- /web/static/img/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/logo-white.png -------------------------------------------------------------------------------- /web/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/logo.png -------------------------------------------------------------------------------- /web/static/img/movie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/movie.jpg -------------------------------------------------------------------------------- /web/static/img/music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/music.png -------------------------------------------------------------------------------- /web/static/img/no-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/no-image.png -------------------------------------------------------------------------------- /web/static/img/opensubtitles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/opensubtitles.png -------------------------------------------------------------------------------- /web/static/img/person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/person.png -------------------------------------------------------------------------------- /web/static/img/pikpak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/pikpak.png -------------------------------------------------------------------------------- /web/static/img/plex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/plex.png -------------------------------------------------------------------------------- /web/static/img/prowlarr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/prowlarr.png -------------------------------------------------------------------------------- /web/static/img/pt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/pt.jpg -------------------------------------------------------------------------------- /web/static/img/pushdeer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/pushdeer.png -------------------------------------------------------------------------------- /web/static/img/pushplus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/pushplus.jpg -------------------------------------------------------------------------------- /web/static/img/qbittorrent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/qbittorrent.png -------------------------------------------------------------------------------- /web/static/img/serverchan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/serverchan.png -------------------------------------------------------------------------------- /web/static/img/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/slack.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1125-2436.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-1125-2436.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1136-640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-1136-640.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1170-2532.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-1170-2532.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1242-2208.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-1242-2208.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1242-2688.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-1242-2688.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1284-2778.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-1284-2778.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1334-750.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-1334-750.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1536-2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-1536-2048.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1620-2160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-1620-2160.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1668-2224.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-1668-2224.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1668-2388.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-1668-2388.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1792-828.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-1792-828.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2048-1536.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-2048-1536.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2048-2732.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-2048-2732.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2160-1620.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-2160-1620.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2208-1242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-2208-1242.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2224-1668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-2224-1668.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2388-1668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-2388-1668.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2436-1125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-2436-1125.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2532-1170.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-2532-1170.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2688-1242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-2688-1242.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2732-2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-2732-2048.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2778-1284.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-2778-1284.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-640-1136.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-640-1136.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-750-1334.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-750-1334.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-828-1792.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/splash/apple-splash-828-1792.png -------------------------------------------------------------------------------- /web/static/img/startup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/startup.jpg -------------------------------------------------------------------------------- /web/static/img/synologychat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/synologychat.png -------------------------------------------------------------------------------- /web/static/img/telegram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/telegram.png -------------------------------------------------------------------------------- /web/static/img/tmdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/tmdb.png -------------------------------------------------------------------------------- /web/static/img/tmdb.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/tmdb.webp -------------------------------------------------------------------------------- /web/static/img/transmission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/transmission.png -------------------------------------------------------------------------------- /web/static/img/tv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/tv.png -------------------------------------------------------------------------------- /web/static/img/users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/users.png -------------------------------------------------------------------------------- /web/static/img/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/web/static/img/wechat.png -------------------------------------------------------------------------------- /web/static/js/FileSaver.min.js: -------------------------------------------------------------------------------- 1 | (function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)}); 2 | 3 | //# sourceMappingURL=FileSaver.min.js.map -------------------------------------------------------------------------------- /web/static/js/demo-theme.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Tabler v1.0.0-beta16 (https://tabler.io) 3 | * @version 1.0.0-beta16 4 | * @link https://tabler.io 5 | * Copyright 2018-2022 The Tabler Authors 6 | * Copyright 2018-2022 codecalm.net Paweł Kuna 7 | * Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE) 8 | */ 9 | !function(e){"function"==typeof define&&define.amd?define(e):e()}((function(){"use strict";var e,t="tablerTheme",n=new Proxy(new URLSearchParams(window.location.search),{get:function(e,t){return e.get(t)}});if(n.theme)localStorage.setItem(t,n.theme),e=n.theme;else{var o=localStorage.getItem(t);e=o||"light"}document.body.classList.remove("theme-dark","theme-light"),document.body.classList.add("theme-".concat(e))})); -------------------------------------------------------------------------------- /web/static/js/demo.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Tabler v1.0.0-beta16 (https://tabler.io) 3 | * @version 1.0.0-beta16 4 | * @link https://tabler.io 5 | * Copyright 2018-2022 The Tabler Authors 6 | * Copyright 2018-2022 codecalm.net Paweł Kuna 7 | * Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE) 8 | */ 9 | !function(t){"function"==typeof define&&define.amd?define(t):t()}((function(){"use strict";function t(t,r){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var r=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null==r)return;var n,o,a=[],l=!0,i=!1;try{for(r=r.call(t);!(l=(n=r.next()).done)&&(a.push(n.value),!e||a.length!==e);l=!0);}catch(t){i=!0,o=t}finally{try{l||null==r.return||r.return()}finally{if(i)throw o}}return a}(t,r)||function(t,r){if(!t)return;if("string"==typeof t)return e(t,r);var n=Object.prototype.toString.call(t).slice(8,-1);"Object"===n&&t.constructor&&(n=t.constructor.name);if("Map"===n||"Set"===n)return Array.from(t);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return e(t,r)}(t,r)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function e(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r', 14 | today: '今天', 15 | month: '月', 16 | week: '周', 17 | day: '日', 18 | list: '日程', 19 | }, 20 | weekText: '周', 21 | allDayText: '全天', 22 | moreLinkText: function(n) { 23 | return '另外 ' + n + ' 个' 24 | }, 25 | noEventsText: '没有事件显示', 26 | }; 27 | 28 | return zhCn; 29 | 30 | }()); 31 | -------------------------------------------------------------------------------- /web/static/js/theme-one_dark.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/one_dark.css",["require","exports","module"],function(e,t,n){n.exports='.ace-one-dark .ace_gutter {\n background: #282c34;\n color: #6a6f7a\n}\n\n.ace-one-dark .ace_print-margin {\n width: 1px;\n background: #e8e8e8\n}\n\n.ace-one-dark {\n background-color: #282c34;\n color: #abb2bf\n}\n\n.ace-one-dark .ace_cursor {\n color: #528bff\n}\n\n.ace-one-dark .ace_marker-layer .ace_selection {\n background: #3d4350\n}\n\n.ace-one-dark.ace_multiselect .ace_selection.ace_start {\n box-shadow: 0 0 3px 0 #282c34;\n border-radius: 2px\n}\n\n.ace-one-dark .ace_marker-layer .ace_step {\n background: #c6dbae\n}\n\n.ace-one-dark .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid #747369\n}\n\n.ace-one-dark .ace_marker-layer .ace_active-line {\n background: rgba(76, 87, 103, .19)\n}\n\n.ace-one-dark .ace_gutter-active-line {\n background-color: rgba(76, 87, 103, .19)\n}\n\n.ace-one-dark .ace_marker-layer .ace_selected-word {\n border: 1px solid #3d4350\n}\n\n.ace-one-dark .ace_fold {\n background-color: #61afef;\n border-color: #abb2bf\n}\n\n.ace-one-dark .ace_keyword {\n color: #c678dd\n}\n\n.ace-one-dark .ace_keyword.ace_operator {\n color: #c678dd\n}\n\n.ace-one-dark .ace_keyword.ace_other.ace_unit {\n color: #d19a66\n}\n\n.ace-one-dark .ace_constant.ace_language {\n color: #d19a66\n}\n\n.ace-one-dark .ace_constant.ace_numeric {\n color: #d19a66\n}\n\n.ace-one-dark .ace_constant.ace_character {\n color: #56b6c2\n}\n\n.ace-one-dark .ace_constant.ace_other {\n color: #56b6c2\n}\n\n.ace-one-dark .ace_support.ace_function {\n color: #61afef\n}\n\n.ace-one-dark .ace_support.ace_constant {\n color: #d19a66\n}\n\n.ace-one-dark .ace_support.ace_class {\n color: #e5c07b\n}\n\n.ace-one-dark .ace_support.ace_type {\n color: #e5c07b\n}\n\n.ace-one-dark .ace_storage {\n color: #c678dd\n}\n\n.ace-one-dark .ace_storage.ace_type {\n color: #c678dd\n}\n\n.ace-one-dark .ace_invalid {\n color: #fff;\n background-color: #f2777a\n}\n\n.ace-one-dark .ace_invalid.ace_deprecated {\n color: #272b33;\n background-color: #d27b53\n}\n\n.ace-one-dark .ace_string {\n color: #98c379\n}\n\n.ace-one-dark .ace_string.ace_regexp {\n color: #e06c75\n}\n\n.ace-one-dark .ace_comment {\n font-style: italic;\n color: #5c6370\n}\n\n.ace-one-dark .ace_variable {\n color: #e06c75\n}\n\n.ace-one-dark .ace_variable.ace_parameter {\n color: #d19a66\n}\n\n.ace-one-dark .ace_meta.ace_tag {\n color: #e06c75\n}\n\n.ace-one-dark .ace_entity.ace_other.ace_attribute-name {\n color: #e06c75\n}\n\n.ace-one-dark .ace_entity.ace_name.ace_function {\n color: #61afef\n}\n\n.ace-one-dark .ace_entity.ace_name.ace_tag {\n color: #e06c75\n}\n\n.ace-one-dark .ace_markup.ace_heading {\n color: #98c379\n}\n\n.ace-one-dark .ace_indent-guide {\n background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ09NrYAgMjP4PAAtGAwchHMyAAAAAAElFTkSuQmCC) right repeat-y\n}\n\n.ace-one-dark .ace_indent-guide-active {\n background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAAZSURBVHjaYvj///9/hivKyv8BAAAA//8DACLqBhbvk+/eAAAAAElFTkSuQmCC") right repeat-y;\n}\n'}),ace.define("ace/theme/one_dark",["require","exports","module","ace/theme/one_dark.css","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-one-dark",t.cssText=e("./one_dark.css");var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass,!1)}); (function() { 2 | ace.require(["ace/theme/one_dark"], function(m) { 3 | if (typeof module == "object" && typeof exports == "object" && module) { 4 | module.exports = m; 5 | } 6 | }); 7 | })(); 8 | -------------------------------------------------------------------------------- /web/static/js/theme-xcode.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/xcode.css",["require","exports","module"],function(e,t,n){n.exports='/* THIS THEME WAS AUTOGENERATED BY Theme.tmpl.css (UUID: EE3AD170-2B7F-4DE1-B724-C75F13FE0085) */\n\n.ace-xcode .ace_gutter {\n background: #e8e8e8;\n color: #333\n}\n\n.ace-xcode .ace_print-margin {\n width: 1px;\n background: #e8e8e8\n}\n\n.ace-xcode {\n background-color: #FFFFFF;\n color: #000000\n}\n\n.ace-xcode .ace_cursor {\n color: #000000\n}\n\n.ace-xcode .ace_marker-layer .ace_selection {\n background: #B5D5FF\n}\n\n.ace-xcode.ace_multiselect .ace_selection.ace_start {\n box-shadow: 0 0 3px 0px #FFFFFF;\n}\n\n.ace-xcode .ace_marker-layer .ace_step {\n background: rgb(198, 219, 174)\n}\n\n.ace-xcode .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid #BFBFBF\n}\n\n.ace-xcode .ace_marker-layer .ace_active-line {\n background: rgba(0, 0, 0, 0.071)\n}\n\n.ace-xcode .ace_gutter-active-line {\n background-color: rgba(0, 0, 0, 0.071)\n}\n\n.ace-xcode .ace_marker-layer .ace_selected-word {\n border: 1px solid #B5D5FF\n}\n\n.ace-xcode .ace_constant.ace_language,\n.ace-xcode .ace_keyword,\n.ace-xcode .ace_meta,\n.ace-xcode .ace_variable.ace_language {\n color: #C800A4\n}\n\n.ace-xcode .ace_invisible {\n color: #BFBFBF\n}\n\n.ace-xcode .ace_constant.ace_character,\n.ace-xcode .ace_constant.ace_other {\n color: #275A5E\n}\n\n.ace-xcode .ace_constant.ace_numeric {\n color: #3A00DC\n}\n\n.ace-xcode .ace_entity.ace_other.ace_attribute-name,\n.ace-xcode .ace_support.ace_constant,\n.ace-xcode .ace_support.ace_function {\n color: #450084\n}\n\n.ace-xcode .ace_fold {\n background-color: #C800A4;\n border-color: #000000\n}\n\n.ace-xcode .ace_entity.ace_name.ace_tag,\n.ace-xcode .ace_support.ace_class,\n.ace-xcode .ace_support.ace_type {\n color: #790EAD\n}\n\n.ace-xcode .ace_storage {\n color: #C900A4\n}\n\n.ace-xcode .ace_string {\n color: #DF0002\n}\n\n.ace-xcode .ace_comment {\n color: #008E00\n}\n\n.ace-xcode .ace_indent-guide {\n background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==) right repeat-y\n}\n\n.ace-xcode .ace_indent-guide-active {\n background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAAZSURBVHjaYvj///9/hivKyv8BAAAA//8DACLqBhbvk+/eAAAAAElFTkSuQmCC") right repeat-y;\n} \n'}),ace.define("ace/theme/xcode",["require","exports","module","ace/theme/xcode.css","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-xcode",t.cssText=e("./xcode.css");var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass,!1)}); (function() { 2 | ace.require(["ace/theme/xcode"], function(m) { 3 | if (typeof module == "object" && typeof exports == "object" && module) { 4 | module.exports = m; 5 | } 6 | }); 7 | })(); 8 | -------------------------------------------------------------------------------- /web/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NAStool", 3 | "short_name": "NAStool", 4 | "start_url": "../", 5 | "icons": [ 6 | { 7 | "src": "./img/logo-black.png", 8 | "sizes": "192x192", 9 | "type": "image/png", 10 | "purpose": "any" 11 | }, 12 | { 13 | "src": "./img/icons/196.png", 14 | "sizes": "192x192", 15 | "type": "image/png", 16 | "purpose": "maskable" 17 | }, 18 | { 19 | "src": "./img/logo-black.png", 20 | "sizes": "512x512", 21 | "type": "image/png", 22 | "purpose": "any" 23 | }, 24 | { 25 | "src": "./img/icons/512.png", 26 | "sizes": "512x512", 27 | "type": "image/png", 28 | "purpose": "maskable" 29 | } 30 | ], 31 | "theme_color": "#000000", 32 | "background_color": "#000000", 33 | "display": "standalone" 34 | } -------------------------------------------------------------------------------- /web/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | {% import 'macro/svg.html' as SVG %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 404 - NAStool 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
404
24 |

出错啦!

25 |

26 | 没有找到这个页面,请检查是不是输错地址了... 27 |

28 | 34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /web/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | {% import 'macro/svg.html' as SVG %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 500 - NAStool 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
500
24 |

出错啦!

25 |

26 | 系统出错了,请检查运行日志看看吧... 27 |

28 | 34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /web/templates/discovery/mediainfo.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/templates/discovery/person.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/templates/discovery/ranking.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/templates/login.html: -------------------------------------------------------------------------------- 1 | {% import 'macro/svg.html' as SVG %} 2 | {% import 'macro/head.html' as HEAD %} 3 | 4 | 5 | 6 | {{ HEAD.meta_link() }} 7 | 登录 - NAStool 8 | 9 | 10 | 11 | 12 | 14 |
15 |
16 |
18 | 19 |
20 |
21 | 23 |
24 | 25 |
26 | 27 | {{ SVG.user() }} 28 | 29 | 31 |
32 | 33 |
34 | 35 | {{ SVG.keyboard() }} 36 | 37 | 39 |
40 |
{{ err_msg }}
41 |
42 | 46 |
47 | 50 |
51 |
52 |
53 |
54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /web/templates/macro/oops.html: -------------------------------------------------------------------------------- 1 | 2 | {% macro nodatafound(title, text) %} 3 |
4 |
5 |
6 |
7 |
8 |

{{ title }}

9 |

10 | {{ text }} 11 |

12 |
13 |
14 |
15 | {% endmacro %} 16 | 17 | 18 | {% macro empty(title, text) %} 19 |
20 |
21 |
22 |
23 |
24 |

{{ title }}

25 |

26 | {{ text }} 27 |

28 |
29 |
30 |
31 | {% endmacro %} 32 | 33 | 34 | {% macro systemerror(title, text) %} 35 |
36 |
37 |
38 |
39 |
40 |

{{ title }}

41 |

42 | {{ text }} 43 |

44 |
45 |
46 |
47 | {% endmacro %} 48 | 49 | 50 | {% macro loading() %} 51 |
52 |
53 |
54 |
55 |
56 |

57 | 页面正在加载,请稍候... 58 |

59 |
60 |
61 |
62 | {% endmacro %} 63 | -------------------------------------------------------------------------------- /web/templates/site/sitelist.html: -------------------------------------------------------------------------------- 1 | {% import 'macro/oops.html' as OOPS %} 2 |
3 | 4 | 13 |
14 | 15 | {% if Count > 0 %} 16 |
17 |
18 | 37 |
38 |
39 | {% else %} 40 | {{ OOPS.nodatafound('没有站点', '没有找到任何站点,请正确维护站点信息。') }} 41 | {% endif %} 42 | -------------------------------------------------------------------------------- /web/templates/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 58 |
59 | 60 |
61 |
62 | 63 | -------------------------------------------------------------------------------- /windows/nas-tools.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/windows/nas-tools.ico -------------------------------------------------------------------------------- /windows/rely/hook-cn2an.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_data_files 2 | 3 | datas = collect_data_files("cn2an") 4 | -------------------------------------------------------------------------------- /windows/rely/hook-zhconv.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_data_files 2 | 3 | datas = collect_data_files("zhconv") 4 | -------------------------------------------------------------------------------- /windows/rely/template.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% if not head %} 5 | 10 | {% else %} 11 | {{ hear | safe }} 12 | {% endif %} 13 | 14 | 15 | {{ body | safe }} 16 | {% for diagram in diagrams %} 17 |
18 |

{{ diagram.title }}

19 |
{{ diagram.text }}
20 |
21 | {{ diagram.svg }} 22 |
23 |
24 | {% endfor %} 25 | 26 | 27 | -------------------------------------------------------------------------------- /windows/rely/upx.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/receyuki/nas-tools/e3a43d4f0896db49de02e9a9201ef2e5877af56f/windows/rely/upx.exe -------------------------------------------------------------------------------- /windows/trayicon.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import webbrowser 4 | 5 | import wx 6 | import wx.adv 7 | 8 | 9 | class Balloon(wx.adv.TaskBarIcon): 10 | ICON = os.path.dirname(__file__).replace("windows", "") + "nas-tools.ico" 11 | 12 | def __init__(self, homepage, log_path): 13 | wx.adv.TaskBarIcon.__init__(self) 14 | self.SetIcon(wx.Icon(self.ICON)) 15 | self.Bind(wx.adv.EVT_TASKBAR_LEFT_DCLICK, self.OnTaskBarLeftDClick) 16 | self.homepage = homepage 17 | self.log_path = log_path 18 | 19 | # Menu数据 20 | def setMenuItemData(self): 21 | return ("Log", self.Onlog), ("Close", self.OnClose) 22 | 23 | # 创建菜单 24 | def CreatePopupMenu(self): 25 | menu = wx.Menu() 26 | for itemName, itemHandler in self.setMenuItemData(): 27 | if not itemName: # itemName为空就添加分隔符 28 | menu.AppendSeparator() 29 | continue 30 | menuItem = wx.MenuItem(None, wx.ID_ANY, text=itemName, kind=wx.ITEM_NORMAL) # 创建菜单项 31 | menu.Append(menuItem) # 将菜单项添加到菜单 32 | self.Bind(wx.EVT_MENU, itemHandler, menuItem) 33 | return menu 34 | 35 | def OnTaskBarLeftDClick(self, event): 36 | webbrowser.open(self.homepage) 37 | 38 | def Onlog(self, event): 39 | os.startfile(self.log_path) 40 | 41 | @staticmethod 42 | def OnClose(event): 43 | exe_name = os.path.basename(sys.executable) 44 | os.system('taskkill /F /IM ' + exe_name) 45 | 46 | 47 | class TrayIcon(wx.Frame): 48 | def __init__(self, homepage, log_path): 49 | app = wx.App() 50 | wx.Frame.__init__(self, None) 51 | self.taskBarIcon = Balloon(homepage, log_path) 52 | webbrowser.open(homepage) 53 | self.Hide() 54 | app.MainLoop() 55 | 56 | 57 | class NullWriter: 58 | softspace = 0 59 | encoding = 'UTF-8' 60 | 61 | def write(*args): 62 | pass 63 | 64 | def flush(*args): 65 | pass 66 | 67 | # Some packages are checking if stdout/stderr is available (e.g., youtube-dl). For details, see #1883. 68 | def isatty(self): 69 | return False 70 | --------------------------------------------------------------------------------