├── app ├── __init__.py ├── message │ ├── client │ │ ├── __init__.py │ │ ├── _base.py │ │ ├── pushdeer.py │ │ ├── iyuu.py │ │ ├── serverchan.py │ │ ├── chanify.py │ │ ├── bark.py │ │ ├── gotify.py │ │ └── pushplus.py │ ├── __init__.py │ └── message_center.py ├── plugins │ ├── modules │ │ ├── __init__.py │ │ ├── libraryrefresh.py │ │ ├── customreleasegroups.py │ │ ├── _base.py │ │ └── synctimer.py │ ├── __init__.py │ └── event_manager.py ├── media │ ├── tmdbv3api │ │ ├── objs │ │ │ ├── __init__.py │ │ │ ├── find.py │ │ │ ├── genre.py │ │ │ ├── episode.py │ │ │ ├── discover.py │ │ │ ├── search.py │ │ │ └── trending.py │ │ ├── exceptions.py │ │ ├── __init__.py │ │ └── as_obj.py │ ├── doubanapi │ │ └── __init__.py │ ├── __init__.py │ └── meta │ │ ├── __init__.py │ │ └── metainfo.py ├── mediaserver │ ├── client │ │ └── __init__.py │ └── __init__.py ├── sites │ ├── siteuserinfo │ │ ├── __init__.py │ │ ├── nexus_project.py │ │ ├── nexus_rabbit.py │ │ └── small_horse.py │ ├── __init__.py │ └── sitesignin │ │ └── _base.py ├── indexer │ ├── __init__.py │ └── client │ │ ├── __init__.py │ │ ├── prowlarr.py │ │ ├── _rarbg.py │ │ └── jackett.py ├── downloader │ ├── __init__.py │ └── client │ │ └── __init__.py ├── conf │ ├── __init__.py │ └── systemconfig.py ├── utils │ ├── exception_utils.py │ ├── nfo_reader.py │ ├── number_utils.py │ ├── cache_manager.py │ ├── json_utils.py │ ├── __init__.py │ ├── dom_utils.py │ ├── tokens.py │ ├── rsstitle_utils.py │ ├── commons.py │ ├── ip_utils.py │ └── mteam_utils.py ├── helper │ ├── redis_helper.py │ ├── thread_helper.py │ ├── __init__.py │ ├── ocr_helper.py │ ├── submodule_helper.py │ ├── display_helper.py │ ├── cookiecloud_helper.py │ ├── progress_helper.py │ ├── site_helper.py │ ├── security_helper.py │ ├── dict_helper.py │ └── ffmpeg_helper.py └── db │ └── __init__.py ├── tests ├── __init__.py ├── cases │ └── __init__.py ├── run.py └── test_metainfo.py ├── web ├── __init__.py ├── backend │ ├── __init__.py │ ├── user.cp310-win_amd64.pyd │ ├── user.cpython-310-darwin.so │ ├── user.cpython-310-x86_64-linux-gnu.so │ ├── user.cpython-310-aarch64-linux-gnu.so │ ├── user.cpython-311-x86_64-linux-musl.so │ └── wallpaper.py ├── static │ ├── img │ │ ├── downloader │ │ │ ├── 115.jpg │ │ │ ├── aria2.png │ │ │ ├── pikpak.png │ │ │ ├── qbittorrent.png │ │ │ └── transmission.png │ │ ├── pt.jpg │ │ ├── tv.png │ │ ├── tmdb.png │ │ ├── jackett.png │ │ ├── movie.jpg │ │ ├── music.png │ │ ├── person.png │ │ ├── startup.jpg │ │ ├── tmdb.webp │ │ ├── users.png │ │ ├── icon-imdb.png │ │ ├── icons │ │ │ ├── 128.png │ │ │ ├── 144.png │ │ │ ├── 152.png │ │ │ ├── 167.png │ │ │ ├── 172.png │ │ │ ├── 180.png │ │ │ ├── 196.png │ │ │ ├── 216.png │ │ │ ├── 256.png │ │ │ ├── 512.png │ │ │ ├── 1024.png │ │ │ ├── 196_ALT.png │ │ │ └── 512_ALT.png │ │ ├── logo │ │ │ ├── logo.png │ │ │ ├── logo-blue.png │ │ │ ├── logo-16x16.png │ │ │ ├── logo-32x32.png │ │ │ ├── logo-black.png │ │ │ └── logo-white.png │ │ ├── no-image.png │ │ ├── prowlarr.png │ │ ├── filetree │ │ │ ├── css.png │ │ │ ├── db.png │ │ │ ├── doc.png │ │ │ ├── pdf.png │ │ │ ├── php.png │ │ │ ├── ppt.png │ │ │ ├── psd.png │ │ │ ├── txt.png │ │ │ ├── xls.png │ │ │ ├── zip.png │ │ │ ├── code.png │ │ │ ├── file.png │ │ │ ├── film.png │ │ │ ├── flash.png │ │ │ ├── html.png │ │ │ ├── java.png │ │ │ ├── linux.png │ │ │ ├── music.png │ │ │ ├── ruby.png │ │ │ ├── picture.png │ │ │ ├── script.png │ │ │ ├── spinner.gif │ │ │ ├── directory.png │ │ │ ├── file-lock.png │ │ │ ├── application.png │ │ │ ├── folder_open.png │ │ │ └── directory-lock.png │ │ ├── message │ │ │ ├── iyuu.png │ │ │ ├── bark.webp │ │ │ ├── gotify.png │ │ │ ├── slack.png │ │ │ ├── wechat.png │ │ │ ├── chanify.png │ │ │ ├── pushdeer.png │ │ │ ├── pushplus.jpg │ │ │ ├── telegram.png │ │ │ ├── serverchan.png │ │ │ └── synologychat.png │ │ ├── plugins │ │ │ ├── emby.png │ │ │ ├── like.jpg │ │ │ ├── nfo.png │ │ │ ├── cloud.png │ │ │ ├── douban.png │ │ │ ├── hosts.png │ │ │ ├── movie.jpg │ │ │ ├── diskusage.jpg │ │ │ ├── refresh.png │ │ │ ├── scraper.png │ │ │ ├── synctimer.png │ │ │ ├── teamwork.png │ │ │ ├── webhook.png │ │ │ ├── cloudflare.jpg │ │ │ ├── SpeedLimiter.jpg │ │ │ ├── autosubtitles.jpeg │ │ │ ├── mediasyncdel.png │ │ │ ├── opensubtitles.png │ │ │ ├── torrentremover.png │ │ │ └── chinesesubfinder.png │ │ ├── sites │ │ │ ├── hdfans.ico │ │ │ ├── hhclub.ico │ │ │ ├── iyuu.png │ │ │ ├── piggo.ico │ │ │ ├── zmpt.ico │ │ │ ├── freefarm.ico │ │ │ ├── hddolby.ico │ │ │ ├── lemonhd.ico │ │ │ ├── sharkpt.ico │ │ │ └── audiences.ico │ │ ├── indexer │ │ │ ├── indexer.jpg │ │ │ ├── indexer.png │ │ │ ├── jackett.png │ │ │ └── prowlarr.png │ │ ├── mediaserver │ │ │ ├── emby.png │ │ │ ├── plex.png │ │ │ ├── jellyfin.jpg │ │ │ └── jellyfin.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 │ ├── components │ │ ├── plugin │ │ │ └── index.js │ │ ├── accordion │ │ │ └── index.js │ │ ├── card │ │ │ ├── index.js │ │ │ ├── normal │ │ │ │ ├── state.js │ │ │ │ └── placeholder.js │ │ │ └── person │ │ │ │ └── index.js │ │ ├── custom │ │ │ └── index.js │ │ ├── layout │ │ │ ├── index.js │ │ │ └── navbar │ │ │ │ └── button.js │ │ ├── page │ │ │ ├── index.js │ │ │ └── person │ │ │ │ └── index.js │ │ ├── lit-index.js │ │ ├── index.js │ │ └── cmd-dialog │ │ │ └── cmd-action.js │ ├── favicon.ico │ ├── js │ │ ├── tabler │ │ │ ├── demo-theme.min.js │ │ │ └── demo.min.js │ │ ├── fullcalendar │ │ │ └── locales │ │ │ │ └── zh-cn.js │ │ ├── modules │ │ │ └── FileSaver.min.js │ │ └── ace │ │ │ └── theme-xcode.js │ ├── site.webmanifest │ └── css │ │ └── nprogress.css ├── robots.txt └── templates │ ├── discovery │ ├── ranking.html │ ├── mediainfo.html │ └── person.html │ ├── 500.html │ ├── 404.html │ ├── site │ └── sitelist.html │ ├── macro │ └── oops.html │ └── test.html ├── version.py ├── db_scripts ├── README ├── script.py.mako ├── versions │ ├── 313bf0f53299_1_2_3.py │ ├── 69508d1aed24_1_2_1.py │ ├── e4a4f6f4e7e4_1_2_4.py │ └── 13a58bd5311f_1_2_2.py └── env.py ├── config ├── scripts │ ├── update_systemdict.sql │ ├── update_downloader.sql │ ├── update_userpris.sql │ ├── update_userrss.sql │ ├── stop_all_service.sql │ └── update_subscribe.sql ├── sites.dat └── pubsites.dat ├── docker ├── volume.png ├── compose.yml ├── dev.Dockerfile ├── Dockerfile └── readme.md ├── package ├── nas-tools.ico ├── rely │ ├── upx.exe │ ├── hook-cn2an.py │ ├── hook-iso639.py │ ├── hook-zhconv.py │ └── template.jinja2 ├── builder │ ├── Dockerfile │ └── alpine.Dockerfile └── trayicon.py ├── third_party.txt ├── .gitignore ├── package_list.txt ├── .github ├── ISSUE_TEMPLATE │ ├── feature.md │ └── bug.md └── workflows │ ├── build-dev.yml │ └── build.yml ├── dbscript_gen.py ├── .gitmodules ├── diff.md ├── README.md └── requirements.txt /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/cases/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/message/client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/plugins/modules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/static/img/downloader/115.jpg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/media/tmdbv3api/objs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mediaserver/client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/sites/siteuserinfo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/static/img/downloader/aria2.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/static/img/downloader/pikpak.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | APP_VERSION = 'v3.1.5-alpha.4' 2 | -------------------------------------------------------------------------------- /web/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /app/indexer/__init__.py: -------------------------------------------------------------------------------- 1 | from .indexer import Indexer 2 | -------------------------------------------------------------------------------- /db_scripts/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /app/downloader/__init__.py: -------------------------------------------------------------------------------- 1 | from .downloader import Downloader 2 | -------------------------------------------------------------------------------- /app/indexer/client/__init__.py: -------------------------------------------------------------------------------- 1 | from .builtin import BuiltinIndexer 2 | -------------------------------------------------------------------------------- /app/mediaserver/__init__.py: -------------------------------------------------------------------------------- 1 | from .media_server import MediaServer 2 | -------------------------------------------------------------------------------- /web/static/components/plugin/index.js: -------------------------------------------------------------------------------- 1 | export * from "./modal/index.js"; -------------------------------------------------------------------------------- /app/media/tmdbv3api/exceptions.py: -------------------------------------------------------------------------------- 1 | class TMDbException(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /config/scripts/update_systemdict.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM SYSTEM_DICT WHERE TYPE like '刷流%'; -------------------------------------------------------------------------------- /web/static/components/accordion/index.js: -------------------------------------------------------------------------------- 1 | export * from "./seasons/index.js"; 2 | -------------------------------------------------------------------------------- /config/sites.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/config/sites.dat -------------------------------------------------------------------------------- /docker/volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/docker/volume.png -------------------------------------------------------------------------------- /app/media/doubanapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .apiv2 import DoubanApi 2 | from .webapi import DoubanWeb 3 | -------------------------------------------------------------------------------- /config/pubsites.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/config/pubsites.dat -------------------------------------------------------------------------------- /config/scripts/update_downloader.sql: -------------------------------------------------------------------------------- 1 | UPDATE DOWNLOADER SET MATCH_PATH = 0 WHERE MATCH_PATH IS NULL -------------------------------------------------------------------------------- /package/nas-tools.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/package/nas-tools.ico -------------------------------------------------------------------------------- /package/rely/upx.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/package/rely/upx.exe -------------------------------------------------------------------------------- /web/static/img/pt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/pt.jpg -------------------------------------------------------------------------------- /web/static/img/tv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/tv.png -------------------------------------------------------------------------------- /app/message/__init__.py: -------------------------------------------------------------------------------- 1 | from .message import Message 2 | from .message_center import MessageCenter 3 | -------------------------------------------------------------------------------- /config/scripts/update_userpris.sql: -------------------------------------------------------------------------------- 1 | UPDATE main.CONFIG_USERS SET PRIS = replace(PRIS, '推荐', '探索') WHERE 1 -------------------------------------------------------------------------------- /web/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/favicon.ico -------------------------------------------------------------------------------- /web/static/img/tmdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/tmdb.png -------------------------------------------------------------------------------- /config/scripts/update_userrss.sql: -------------------------------------------------------------------------------- 1 | UPDATE CONFIG_USER_RSS SET PROCESS_COUNT = '0' WHERE PROCESS_COUNT is null -------------------------------------------------------------------------------- /web/static/components/card/index.js: -------------------------------------------------------------------------------- 1 | export * from "./normal/index.js"; 2 | export * from "./person/index.js"; -------------------------------------------------------------------------------- /web/static/components/custom/index.js: -------------------------------------------------------------------------------- 1 | export * from "./img/index.js"; 2 | export * from "./slide/index.js"; -------------------------------------------------------------------------------- /web/static/img/jackett.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/jackett.png -------------------------------------------------------------------------------- /web/static/img/movie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/movie.jpg -------------------------------------------------------------------------------- /web/static/img/music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/music.png -------------------------------------------------------------------------------- /web/static/img/person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/person.png -------------------------------------------------------------------------------- /web/static/img/startup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/startup.jpg -------------------------------------------------------------------------------- /web/static/img/tmdb.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/tmdb.webp -------------------------------------------------------------------------------- /web/static/img/users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/users.png -------------------------------------------------------------------------------- /web/templates/discovery/ranking.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /third_party.txt: -------------------------------------------------------------------------------- 1 | feapder 2 | qbittorrent-api/src 3 | anitopy 4 | plexapi 5 | transmission-rpc 6 | slack_bolt -------------------------------------------------------------------------------- /web/static/components/layout/index.js: -------------------------------------------------------------------------------- 1 | export * from "./navbar/index.js"; 2 | export * from "./searchbar/index.js"; -------------------------------------------------------------------------------- /web/static/img/icon-imdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/icon-imdb.png -------------------------------------------------------------------------------- /web/static/img/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/icons/128.png -------------------------------------------------------------------------------- /web/static/img/icons/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/icons/144.png -------------------------------------------------------------------------------- /web/static/img/icons/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/icons/152.png -------------------------------------------------------------------------------- /web/static/img/icons/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/icons/167.png -------------------------------------------------------------------------------- /web/static/img/icons/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/icons/172.png -------------------------------------------------------------------------------- /web/static/img/icons/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/icons/180.png -------------------------------------------------------------------------------- /web/static/img/icons/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/icons/196.png -------------------------------------------------------------------------------- /web/static/img/icons/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/icons/216.png -------------------------------------------------------------------------------- /web/static/img/icons/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/icons/256.png -------------------------------------------------------------------------------- /web/static/img/icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/icons/512.png -------------------------------------------------------------------------------- /web/static/img/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/logo/logo.png -------------------------------------------------------------------------------- /web/static/img/no-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/no-image.png -------------------------------------------------------------------------------- /web/static/img/prowlarr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/prowlarr.png -------------------------------------------------------------------------------- /app/downloader/client/__init__.py: -------------------------------------------------------------------------------- 1 | from .qbittorrent import Qbittorrent 2 | from .transmission import Transmission 3 | -------------------------------------------------------------------------------- /web/static/img/filetree/css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/css.png -------------------------------------------------------------------------------- /web/static/img/filetree/db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/db.png -------------------------------------------------------------------------------- /web/static/img/filetree/doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/doc.png -------------------------------------------------------------------------------- /web/static/img/filetree/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/pdf.png -------------------------------------------------------------------------------- /web/static/img/filetree/php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/php.png -------------------------------------------------------------------------------- /web/static/img/filetree/ppt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/ppt.png -------------------------------------------------------------------------------- /web/static/img/filetree/psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/psd.png -------------------------------------------------------------------------------- /web/static/img/filetree/txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/txt.png -------------------------------------------------------------------------------- /web/static/img/filetree/xls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/xls.png -------------------------------------------------------------------------------- /web/static/img/filetree/zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/zip.png -------------------------------------------------------------------------------- /web/static/img/icons/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/icons/1024.png -------------------------------------------------------------------------------- /web/static/img/message/iyuu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/message/iyuu.png -------------------------------------------------------------------------------- /web/static/img/plugins/emby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/emby.png -------------------------------------------------------------------------------- /web/static/img/plugins/like.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/like.jpg -------------------------------------------------------------------------------- /web/static/img/plugins/nfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/nfo.png -------------------------------------------------------------------------------- /web/static/img/sites/hdfans.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/sites/hdfans.ico -------------------------------------------------------------------------------- /web/static/img/sites/hhclub.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/sites/hhclub.ico -------------------------------------------------------------------------------- /web/static/img/sites/iyuu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/sites/iyuu.png -------------------------------------------------------------------------------- /web/static/img/sites/piggo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/sites/piggo.ico -------------------------------------------------------------------------------- /web/static/img/sites/zmpt.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/sites/zmpt.ico -------------------------------------------------------------------------------- /web/static/img/filetree/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/code.png -------------------------------------------------------------------------------- /web/static/img/filetree/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/file.png -------------------------------------------------------------------------------- /web/static/img/filetree/film.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/film.png -------------------------------------------------------------------------------- /web/static/img/filetree/flash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/flash.png -------------------------------------------------------------------------------- /web/static/img/filetree/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/html.png -------------------------------------------------------------------------------- /web/static/img/filetree/java.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/java.png -------------------------------------------------------------------------------- /web/static/img/filetree/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/linux.png -------------------------------------------------------------------------------- /web/static/img/filetree/music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/music.png -------------------------------------------------------------------------------- /web/static/img/filetree/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/ruby.png -------------------------------------------------------------------------------- /web/static/img/icons/196_ALT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/icons/196_ALT.png -------------------------------------------------------------------------------- /web/static/img/icons/512_ALT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/icons/512_ALT.png -------------------------------------------------------------------------------- /web/static/img/logo/logo-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/logo/logo-blue.png -------------------------------------------------------------------------------- /web/static/img/message/bark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/message/bark.webp -------------------------------------------------------------------------------- /web/static/img/message/gotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/message/gotify.png -------------------------------------------------------------------------------- /web/static/img/message/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/message/slack.png -------------------------------------------------------------------------------- /web/static/img/message/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/message/wechat.png -------------------------------------------------------------------------------- /web/static/img/plugins/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/cloud.png -------------------------------------------------------------------------------- /web/static/img/plugins/douban.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/douban.png -------------------------------------------------------------------------------- /web/static/img/plugins/hosts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/hosts.png -------------------------------------------------------------------------------- /web/static/img/plugins/movie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/movie.jpg -------------------------------------------------------------------------------- /web/static/img/sites/freefarm.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/sites/freefarm.ico -------------------------------------------------------------------------------- /web/static/img/sites/hddolby.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/sites/hddolby.ico -------------------------------------------------------------------------------- /web/static/img/sites/lemonhd.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/sites/lemonhd.ico -------------------------------------------------------------------------------- /web/static/img/sites/sharkpt.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/sites/sharkpt.ico -------------------------------------------------------------------------------- /web/templates/discovery/mediainfo.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/backend/user.cp310-win_amd64.pyd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/backend/user.cp310-win_amd64.pyd -------------------------------------------------------------------------------- /web/static/img/filetree/picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/picture.png -------------------------------------------------------------------------------- /web/static/img/filetree/script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/script.png -------------------------------------------------------------------------------- /web/static/img/filetree/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/spinner.gif -------------------------------------------------------------------------------- /web/static/img/indexer/indexer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/indexer/indexer.jpg -------------------------------------------------------------------------------- /web/static/img/indexer/indexer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/indexer/indexer.png -------------------------------------------------------------------------------- /web/static/img/indexer/jackett.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/indexer/jackett.png -------------------------------------------------------------------------------- /web/static/img/indexer/prowlarr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/indexer/prowlarr.png -------------------------------------------------------------------------------- /web/static/img/logo/logo-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/logo/logo-16x16.png -------------------------------------------------------------------------------- /web/static/img/logo/logo-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/logo/logo-32x32.png -------------------------------------------------------------------------------- /web/static/img/logo/logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/logo/logo-black.png -------------------------------------------------------------------------------- /web/static/img/logo/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/logo/logo-white.png -------------------------------------------------------------------------------- /web/static/img/mediaserver/emby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/mediaserver/emby.png -------------------------------------------------------------------------------- /web/static/img/mediaserver/plex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/mediaserver/plex.png -------------------------------------------------------------------------------- /web/static/img/message/chanify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/message/chanify.png -------------------------------------------------------------------------------- /web/static/img/message/pushdeer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/message/pushdeer.png -------------------------------------------------------------------------------- /web/static/img/message/pushplus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/message/pushplus.jpg -------------------------------------------------------------------------------- /web/static/img/message/telegram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/message/telegram.png -------------------------------------------------------------------------------- /web/static/img/plugins/diskusage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/diskusage.jpg -------------------------------------------------------------------------------- /web/static/img/plugins/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/refresh.png -------------------------------------------------------------------------------- /web/static/img/plugins/scraper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/scraper.png -------------------------------------------------------------------------------- /web/static/img/plugins/synctimer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/synctimer.png -------------------------------------------------------------------------------- /web/static/img/plugins/teamwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/teamwork.png -------------------------------------------------------------------------------- /web/static/img/plugins/webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/webhook.png -------------------------------------------------------------------------------- /web/static/img/sites/audiences.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/sites/audiences.ico -------------------------------------------------------------------------------- /package/rely/hook-cn2an.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_data_files 2 | 3 | datas = collect_data_files("cn2an") 4 | -------------------------------------------------------------------------------- /package/rely/hook-iso639.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_data_files 2 | 3 | datas = collect_data_files("iso639") 4 | -------------------------------------------------------------------------------- /package/rely/hook-zhconv.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_data_files 2 | 3 | datas = collect_data_files("zhconv") 4 | -------------------------------------------------------------------------------- /web/backend/user.cpython-310-darwin.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/backend/user.cpython-310-darwin.so -------------------------------------------------------------------------------- /web/static/img/filetree/directory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/directory.png -------------------------------------------------------------------------------- /web/static/img/filetree/file-lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/file-lock.png -------------------------------------------------------------------------------- /web/static/img/message/serverchan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/message/serverchan.png -------------------------------------------------------------------------------- /web/static/img/plugins/cloudflare.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/cloudflare.jpg -------------------------------------------------------------------------------- /app/conf/__init__.py: -------------------------------------------------------------------------------- 1 | from .systemconfig import SystemConfig 2 | from .moduleconf import ModuleConf 3 | from .siteconf import SiteConf 4 | -------------------------------------------------------------------------------- /app/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | from .event_manager import EventManager, EventHandler, Event 2 | from .plugin_manager import PluginManager 3 | -------------------------------------------------------------------------------- /config/scripts/stop_all_service.sql: -------------------------------------------------------------------------------- 1 | UPDATE SITE_BRUSH_TASK SET STATE = 'N' WHERE 1; 2 | UPDATE TORRENT_REMOVE_TASK SET ENABLED = 0 WHERE 1; -------------------------------------------------------------------------------- /web/static/img/downloader/qbittorrent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/downloader/qbittorrent.png -------------------------------------------------------------------------------- /web/static/img/filetree/application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/application.png -------------------------------------------------------------------------------- /web/static/img/filetree/folder_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/folder_open.png -------------------------------------------------------------------------------- /web/static/img/mediaserver/jellyfin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/mediaserver/jellyfin.jpg -------------------------------------------------------------------------------- /web/static/img/mediaserver/jellyfin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/mediaserver/jellyfin.png -------------------------------------------------------------------------------- /web/static/img/message/synologychat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/message/synologychat.png -------------------------------------------------------------------------------- /web/static/img/plugins/SpeedLimiter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/SpeedLimiter.jpg -------------------------------------------------------------------------------- /web/static/img/plugins/autosubtitles.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/autosubtitles.jpeg -------------------------------------------------------------------------------- /web/static/img/plugins/mediasyncdel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/mediasyncdel.png -------------------------------------------------------------------------------- /web/static/img/plugins/opensubtitles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/opensubtitles.png -------------------------------------------------------------------------------- /web/static/img/plugins/torrentremover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/torrentremover.png -------------------------------------------------------------------------------- /web/static/img/downloader/transmission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/downloader/transmission.png -------------------------------------------------------------------------------- /web/static/img/filetree/directory-lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/filetree/directory-lock.png -------------------------------------------------------------------------------- /web/static/img/plugins/chinesesubfinder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/plugins/chinesesubfinder.png -------------------------------------------------------------------------------- /web/backend/user.cpython-310-x86_64-linux-gnu.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/backend/user.cpython-310-x86_64-linux-gnu.so -------------------------------------------------------------------------------- /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/img/splash/apple-splash-1125-2436.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-1125-2436.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1136-640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-1136-640.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1170-2532.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-1170-2532.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1242-2208.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-1242-2208.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1242-2688.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-1242-2688.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1284-2778.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-1284-2778.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1334-750.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-1334-750.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1536-2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-1536-2048.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1620-2160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-1620-2160.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1668-2224.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-1668-2224.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1668-2388.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-1668-2388.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1792-828.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-1792-828.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2048-1536.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-2048-1536.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2048-2732.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-2048-2732.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2160-1620.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-2160-1620.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2208-1242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-2208-1242.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2224-1668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-2224-1668.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2388-1668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-2388-1668.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2436-1125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-2436-1125.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2532-1170.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-2532-1170.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2688-1242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-2688-1242.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2732-2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-2732-2048.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2778-1284.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-2778-1284.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-640-1136.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-640-1136.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-750-1334.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-750-1334.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-828-1792.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/static/img/splash/apple-splash-828-1792.png -------------------------------------------------------------------------------- /web/backend/user.cpython-310-aarch64-linux-gnu.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/backend/user.cpython-310-aarch64-linux-gnu.so -------------------------------------------------------------------------------- /web/backend/user.cpython-311-x86_64-linux-musl.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xforee/nas-tools-bk/HEAD/web/backend/user.cpython-311-x86_64-linux-musl.so -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/** 2 | *.pyc 3 | *.c 4 | /test.py 5 | /setup.py 6 | /build_sites.py 7 | /web/backend/user.py 8 | /build/** 9 | /venv/** 10 | /cloudflarespeedtest/** -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/scripts/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /web/templates/discovery/person.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/sites/__init__.py: -------------------------------------------------------------------------------- 1 | from app.sites.site_userinfo import SiteUserInfo 2 | from .sites import Sites 3 | from .site_cookie import SiteCookie 4 | from .site_signin import SiteSignin 5 | from .site_subtitle import SiteSubtitle 6 | from .siteconf import SiteConf 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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"; 5 | export * from "./plugin/index.js"; 6 | export * from "./accordion/index.js"; 7 | export * from "./cmd-dialog/index.js"; -------------------------------------------------------------------------------- /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 | fuse3 14 | xvfb 15 | inotify-tools 16 | chromium-chromedriver 17 | npm 18 | dumb-init 19 | ffmpeg 20 | redis 21 | wget 22 | shadow 23 | sudo 24 | -------------------------------------------------------------------------------- /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(); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /app/helper/redis_helper.py: -------------------------------------------------------------------------------- 1 | from app.utils import SystemUtils 2 | 3 | 4 | class RedisHelper: 5 | 6 | @staticmethod 7 | def is_valid(): 8 | """ 9 | 判斷redis是否有效 10 | """ 11 | if SystemUtils.is_docker(): 12 | return True if SystemUtils.execute("which redis-server") else False 13 | else: 14 | return False 15 | -------------------------------------------------------------------------------- /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/utils/nfo_reader.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | 4 | class NfoReader: 5 | def __init__(self, xml_file_path): 6 | self.xml_file_path = xml_file_path 7 | self.tree = ET.parse(xml_file_path) 8 | self.root = self.tree.getroot() 9 | 10 | def get_element_value(self, element_path): 11 | element = self.root.find(element_path) 12 | return element.text if element is not None else None 13 | -------------------------------------------------------------------------------- /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/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 | 14 | @staticmethod 15 | def get_size_gb(size): 16 | """ 17 | 将字节转换为GB 18 | """ 19 | if not size: 20 | return 0.0 21 | return float(size) / 1024 / 1024 / 1024 22 | -------------------------------------------------------------------------------- /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 | 16 | OpenAISessionCache = Cache(maxsize=100, ttl=3600, timer=time.time, default=None) 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 问题模板 3 | about: 如发现Bug,请按此模板提交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 | ## 是否已经浏览过Issues、Wiki及TG公众号仍无法解决? 18 | 19 | > 请搜索Issues列表、查看wiki跟TG公众号的更新说明,已经解释过的问题不要重复提问 20 | 21 | 22 | ## 你期望的结果 23 | 24 | > 描述以下你期望的结果 25 | 26 | ## 给出程序界面截图、后台运行日志或配置文件 27 | 28 | > 如UI BUG请提供截图及配置文件截图 29 | > 其它问题提供后台日志,如为Docker请提供docker的日志 30 | -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docker/compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | nas-tools: 4 | image: 0xforee/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 | restart: always 16 | network_mode: bridge 17 | hostname: nas-tools 18 | container_name: nas-tools -------------------------------------------------------------------------------- /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/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/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, OpenAISessionCache 12 | from .exception_utils import ExceptionUtils 13 | from .rsstitle_utils import RssTitleUtils 14 | from .nfo_reader import NfoReader 15 | from .ip_utils import IpUtils 16 | from .mteam_utils import MteamUtils 17 | -------------------------------------------------------------------------------- /package/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 | -------------------------------------------------------------------------------- /web/static/js/tabler/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))})); -------------------------------------------------------------------------------- /app/helper/__init__.py: -------------------------------------------------------------------------------- 1 | from .chrome_helper import ChromeHelper, init_chrome 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 .words_helper import WordsHelper 13 | from .submodule_helper import SubmoduleHelper 14 | from .cookiecloud_helper import CookieCloudHelper 15 | from .ffmpeg_helper import FfmpegHelper 16 | from .redis_helper import RedisHelper 17 | -------------------------------------------------------------------------------- /web/static/js/fullcalendar/locales/zh-cn.js: -------------------------------------------------------------------------------- 1 | FullCalendar.globalLocales.push(function () { 2 | 'use strict'; 3 | 4 | return { 5 | code: 'zh-cn', 6 | week: { 7 | // GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效 8 | dow: 1, // Monday is the first day of the week. 9 | doy: 4, // The week that contains Jan 4th is the first week of the year. 10 | }, 11 | buttonText: { 12 | prev: '<', 13 | next: '>', 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 | }()); 29 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_party/feapder"] 2 | path = third_party/feapder 3 | url = https://github.com/0xforee/feapder 4 | [submodule "third_party/qbittorrent-api"] 5 | path = third_party/qbittorrent-api 6 | url = https://github.com/rmartin16/qbittorrent-api 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/transmission-rpc"] 14 | path = third_party/transmission-rpc 15 | url = https://github.com/trim21/transmission-rpc 16 | [submodule "third_party/slack_bolt"] 17 | path = third_party/slack_bolt 18 | url = https://github.com/slackapi/bolt-python 19 | -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /db_scripts/versions/313bf0f53299_1_2_3.py: -------------------------------------------------------------------------------- 1 | """1.2.3 2 | 3 | Revision ID: 313bf0f53299 4 | Revises: 13a58bd5311f 5 | Create Date: 2023-07-23 00:10:11.089367 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '313bf0f53299' 14 | down_revision = '13a58bd5311f' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | # 1.2.1 22 | try: 23 | with op.batch_alter_table("SITE_BRUSH_TASK") as batch_op: 24 | batch_op.add_column(sa.Column('FRACTION_RULE', sa.Text, nullable=True)) 25 | except Exception as e: 26 | pass 27 | # ### end Alembic commands ### 28 | 29 | def downgrade() -> None: 30 | pass 31 | -------------------------------------------------------------------------------- /db_scripts/versions/69508d1aed24_1_2_1.py: -------------------------------------------------------------------------------- 1 | """1.2.1 2 | 3 | Revision ID: 69508d1aed24 4 | Revises: 6abeaa9ece15 5 | Create Date: 2023-03-24 11:12:51.646014 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '69508d1aed24' 14 | down_revision = '6abeaa9ece15' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | # 1.2.1 22 | try: 23 | with op.batch_alter_table("SITE_BRUSH_TASK") as batch_op: 24 | batch_op.add_column(sa.Column('RSSURL', sa.Text, nullable=True)) 25 | except Exception as e: 26 | pass 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade() -> None: 31 | pass 32 | -------------------------------------------------------------------------------- /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/sites/sitesignin/_base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from abc import ABCMeta, abstractmethod 3 | 4 | from app.utils import StringUtils 5 | 6 | 7 | class _ISiteSigninHandler(metaclass=ABCMeta): 8 | """ 9 | 实现站点签到的基类,所有站点签到类都需要继承此类,并实现match和signin方法 10 | 实现类放置到sitesignin目录下将会自动加载 11 | """ 12 | # 匹配的站点Url,每一个实现类都需要设置为自己的站点Url 13 | site_url = "" 14 | 15 | @abstractmethod 16 | def match(self, url): 17 | """ 18 | 根据站点Url判断是否匹配当前站点签到类,大部分情况使用默认实现即可 19 | :param url: 站点Url 20 | :return: 是否匹配,如匹配则会调用该类的signin方法 21 | """ 22 | return True if StringUtils.url_equal(url, self.site_url) else False 23 | 24 | @abstractmethod 25 | def signin(self, site_info: dict): 26 | """ 27 | 执行签到操作 28 | :param site_info: 站点信息,含有站点Url、站点Cookie、UA等信息 29 | :return: 签到结果信息 30 | """ 31 | pass 32 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /web/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NAStool", 3 | "short_name": "NAStool", 4 | "start_url": "../", 5 | "icons": [ 6 | { 7 | "src": "./img/logo/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/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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db_scripts/versions/e4a4f6f4e7e4_1_2_4.py: -------------------------------------------------------------------------------- 1 | """1.2.3 2 | 3 | Revision ID: 313bf0f53299 4 | Revises: 13a58bd5311f 5 | Create Date: 2023-07-23 00:10:11.089367 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e4a4f6f4e7e4' 14 | down_revision = '313bf0f53299' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | # 1.2.1 22 | try: 23 | with op.batch_alter_table("SITE_BRUSH_TASK") as batch_op: 24 | batch_op.add_column(sa.Column('BRUSHTASK_FREE_LIMIT_SPEED', sa.Text, nullable=True)) 25 | except Exception as e: 26 | pass 27 | 28 | try: 29 | with op.batch_alter_table("SITE_BRUSH_TORRENTS") as batch_op: 30 | batch_op.add_column(sa.Column('FREE_DEADLINE', sa.Text, nullable=True)) 31 | except Exception as e: 32 | pass 33 | # ### end Alembic commands ### 34 | 35 | def downgrade() -> None: 36 | pass 37 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /web/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | {% import 'macro/svg.html' as SVG %} 3 | {% import 'macro/head.html' as HEAD %} 4 | 5 | 6 | 7 | {{ HEAD.meta_link() }} 8 | 500 - NAStool 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
500
18 |

出错啦!

19 |

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

22 | 28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /web/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | {% import 'macro/svg.html' as SVG %} 3 | {% import 'macro/head.html' as HEAD %} 4 | 5 | 6 | 7 | {{ HEAD.meta_link() }} 8 | 404 - NAStool 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
404
18 |

出错啦!

19 |

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

22 | 28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /db_scripts/versions/13a58bd5311f_1_2_2.py: -------------------------------------------------------------------------------- 1 | """1.2.2 2 | 3 | Revision ID: 13a58bd5311f 4 | Revises: 69508d1aed24 5 | Create Date: 2023-04-04 08:49:43.453901 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '13a58bd5311f' 14 | down_revision = '69508d1aed24' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | # 1.2.2 22 | try: 23 | with op.batch_alter_table("RSS_TVS") as batch_op: 24 | batch_op.add_column(sa.Column('FILTER_INCLUDE', sa.Text, nullable=True)) 25 | batch_op.add_column(sa.Column('FILTER_EXCLUDE', sa.Text, nullable=True)) 26 | except Exception as e: 27 | pass 28 | try: 29 | with op.batch_alter_table("RSS_MOVIES") as batch_op: 30 | batch_op.add_column(sa.Column('FILTER_INCLUDE', sa.Text, nullable=True)) 31 | batch_op.add_column(sa.Column('FILTER_EXCLUDE', sa.Text, nullable=True)) 32 | except Exception as e: 33 | pass 34 | # ### end Alembic commands ### 35 | 36 | 37 | def downgrade() -> None: 38 | pass 39 | -------------------------------------------------------------------------------- /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/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.stop_service() 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 stop_service(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.stop_service() 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.normpath(os.path.join(Config().get_config_path(), 'user.db')) 35 | script_location = os.path.normpath(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 | log.console('数据库更新完成') 43 | except Exception as e: 44 | log.console(f'数据库更新失败:{e}') 45 | -------------------------------------------------------------------------------- /package/builder/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.10-bullseye AS Builder 2 | 3 | ARG branch 4 | 5 | ENV NASTOOL_CONFIG=/nas-tools/config/config.yaml 6 | ENV py_site_packages=/usr/local/lib/python3.10/site-packages 7 | 8 | RUN python -m pip install --upgrade pip setuptools 9 | RUN pip install wheel cython pyinstaller==5.7.0 10 | RUN git clone --depth=1 -b ${branch} https://github.com/NAStool/nas-tools --recurse-submodule /nas-tools 11 | WORKDIR /nas-tools 12 | RUN pip install -r requirements.txt 13 | RUN pip install pyparsing 14 | RUN cp ./package/rely/hook-cn2an.py ${py_site_packages}/PyInstaller/hooks/ && \ 15 | cp ./package/rely/hook-zhconv.py ${py_site_packages}/PyInstaller/hooks/ && \ 16 | cp ./package/rely/hook-iso639.py ${py_site_packages}/PyInstaller/hooks/ && \ 17 | cp ./third_party.txt ./package/ && \ 18 | mkdir -p ${py_site_packages}/setuptools/_vendor/pyparsing/diagram/ && \ 19 | cp ./package/rely/template.jinja2 ${py_site_packages}/setuptools/_vendor/pyparsing/diagram/ && \ 20 | cp -r ./web/. ${py_site_packages}/web/ && \ 21 | cp -r ./config/. ${py_site_packages}/config/ && \ 22 | cp -r ./db_scripts/. ${py_site_packages}/db_scripts/ 23 | WORKDIR /nas-tools/package 24 | RUN pyinstaller nas-tools.spec 25 | RUN ls -al /nas-tools/package/dist/ 26 | WORKDIR /rootfs 27 | RUN cp /nas-tools/package/dist/nas-tools . 28 | 29 | FROM scratch 30 | 31 | COPY --from=Builder /rootfs/nas-tools /nas-tools -------------------------------------------------------------------------------- /package/builder/alpine.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.10-alpine AS Builder 2 | 3 | ARG branch 4 | 5 | ENV NASTOOL_CONFIG=/nas-tools/config/config.yaml 6 | ENV py_site_packages=/usr/local/lib/python3.10/site-packages 7 | 8 | RUN apk add build-base git libxslt-dev libxml2-dev musl-dev gcc libffi-dev 9 | RUN pip install --upgrade pip setuptools 10 | RUN pip install wheel cython pyinstaller==5.7.0 11 | RUN git clone --depth=1 -b ${branch} https://github.com/NAStool/nas-tools --recurse-submodule /nas-tools 12 | WORKDIR /nas-tools 13 | RUN pip install -r requirements.txt 14 | RUN pip install pyparsing 15 | RUN cp ./package/rely/hook-cn2an.py ${py_site_packages}/PyInstaller/hooks/ && \ 16 | cp ./package/rely/hook-zhconv.py ${py_site_packages}/PyInstaller/hooks/ && \ 17 | cp ./package/rely/hook-iso639.py ${py_site_packages}/PyInstaller/hooks/ && \ 18 | cp ./third_party.txt ./package/ && \ 19 | mkdir -p ${py_site_packages}/setuptools/_vendor/pyparsing/diagram/ && \ 20 | cp ./package/rely/template.jinja2 ${py_site_packages}/setuptools/_vendor/pyparsing/diagram/ && \ 21 | cp -r ./web/. ${py_site_packages}/web/ && \ 22 | cp -r ./config/. ${py_site_packages}/config/ && \ 23 | cp -r ./db_scripts/. ${py_site_packages}/db_scripts/ 24 | WORKDIR /nas-tools/package 25 | RUN pyinstaller nas-tools.spec 26 | RUN ls -al /nas-tools/package/dist/ 27 | WORKDIR /rootfs 28 | RUN cp /nas-tools/package/dist/nas-tools . 29 | 30 | FROM scratch 31 | 32 | COPY --from=Builder /rootfs/nas-tools /nas-tools -------------------------------------------------------------------------------- /app/helper/cookiecloud_helper.py: -------------------------------------------------------------------------------- 1 | from app.utils import RequestUtils 2 | 3 | 4 | class CookieCloudHelper(object): 5 | _req = None 6 | _server = None 7 | _key = None 8 | _password = None 9 | 10 | def __init__(self, server, key, password): 11 | self._server = server 12 | if self._server: 13 | if not self._server.startswith("http"): 14 | self._server = "http://%s" % self._server 15 | if self._server.endswith("/"): 16 | self._server = self._server[:-1] 17 | self._key = key 18 | self._password = password 19 | self._req = RequestUtils(content_type="application/json") 20 | 21 | def download_data(self): 22 | """ 23 | 从CookieCloud下载数据 24 | """ 25 | if not self._server or not self._key or not self._password: 26 | return {}, "CookieCloud参数不正确" 27 | req_url = "%s/get/%s" % (self._server, self._key) 28 | ret = self._req.post_res(url=req_url, json={"password": self._password}) 29 | if ret and ret.status_code == 200: 30 | result = ret.json() 31 | if not result: 32 | return {}, "" 33 | if result.get("cookie_data"): 34 | return result.get("cookie_data"), "" 35 | return result, "" 36 | elif ret: 37 | return {}, "同步CookieCloud失败,错误码:%s" % ret.status_code 38 | else: 39 | return {}, "CookieCloud请求失败,请检查服务器地址、用户KEY及加密密码是否正确" 40 | -------------------------------------------------------------------------------- /.github/workflows/build-dev.yml: -------------------------------------------------------------------------------- 1 | name: NAStool Docker Dev 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - dev 7 | paths: 8 | - version.py 9 | - .github/workflows/build-dev.yml 10 | - package_list.txt 11 | - requirements.txt 12 | - docker/dev.Dockerfile 13 | - docker/entrypoint.sh 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | name: Build Docker Image 18 | steps: 19 | - 20 | name: Checkout 21 | uses: actions/checkout@master 22 | 23 | - 24 | name: Release version 25 | id: release_version 26 | run: | 27 | app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp") 28 | echo "app_version=$app_version" >> $GITHUB_ENV 29 | 30 | - 31 | name: Set Up QEMU 32 | uses: docker/setup-qemu-action@v1 33 | 34 | - 35 | name: Set Up Buildx 36 | uses: docker/setup-buildx-action@v1 37 | 38 | - 39 | name: Login DockerHub 40 | uses: docker/login-action@v1 41 | with: 42 | username: ${{ secrets.DOCKER_USERNAME }} 43 | password: ${{ secrets.DOCKER_PASSWORD }} 44 | 45 | - name: Build Image 46 | uses: docker/build-push-action@v2 47 | with: 48 | context: . 49 | file: docker/dev.Dockerfile 50 | platforms: | 51 | linux/amd64 52 | linux/arm64 53 | push: true 54 | tags: | 55 | ${{ secrets.DOCKER_USERNAME }}/nas-tools:${{ env.app_version }}-beta -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/utils/commons.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import threading 3 | import time 4 | 5 | # 线程锁 6 | lock = threading.RLock() 7 | 8 | # 全局实例 9 | INSTANCES = {} 10 | 11 | 12 | # 单例模式注解 13 | def singleton(cls): 14 | # 创建字典用来保存类的实例对象 15 | global INSTANCES 16 | 17 | def _singleton(*args, **kwargs): 18 | # 先判断这个类有没有对象 19 | if cls not in INSTANCES: 20 | with lock: 21 | if cls not in INSTANCES: 22 | INSTANCES[cls] = cls(*args, **kwargs) 23 | pass 24 | # 将实例对象返回 25 | return INSTANCES[cls] 26 | 27 | return _singleton 28 | 29 | 30 | # 重试装饰器 31 | def retry(ExceptionToCheck, tries=3, delay=3, backoff=2, logger=None): 32 | """ 33 | :param ExceptionToCheck: 需要捕获的异常 34 | :param tries: 重试次数 35 | :param delay: 延迟时间 36 | :param backoff: 延迟倍数 37 | :param logger: 日志对象 38 | """ 39 | 40 | def deco_retry(f): 41 | def f_retry(*args, **kwargs): 42 | mtries, mdelay = tries, delay 43 | while mtries > 1: 44 | try: 45 | return f(*args, **kwargs) 46 | except ExceptionToCheck as e: 47 | msg = f"{str(e)}, {mdelay} 秒后重试 ..." 48 | if logger: 49 | logger.warn(msg) 50 | else: 51 | print(msg) 52 | time.sleep(mdelay) 53 | mtries -= 1 54 | mdelay *= backoff 55 | return f(*args, **kwargs) 56 | 57 | return f_retry 58 | 59 | return deco_retry 60 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: NAStool Docker 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - version.py 9 | - .github/workflows/build.yml 10 | - package_list.txt 11 | - requirements.txt 12 | - docker/Dockerfile 13 | - docker/entrypoint.sh 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | name: Build Docker Image 18 | steps: 19 | - 20 | name: Checkout 21 | uses: actions/checkout@master 22 | 23 | - 24 | name: Release version 25 | id: release_version 26 | run: | 27 | app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp") 28 | echo "app_version=$app_version" >> $GITHUB_ENV 29 | 30 | - 31 | name: Set Up QEMU 32 | uses: docker/setup-qemu-action@v1 33 | 34 | - 35 | name: Set Up Buildx 36 | uses: docker/setup-buildx-action@v1 37 | 38 | - 39 | name: Login DockerHub 40 | uses: docker/login-action@v1 41 | with: 42 | username: ${{ secrets.DOCKER_USERNAME }} 43 | password: ${{ secrets.DOCKER_PASSWORD }} 44 | 45 | - name: Build Image 46 | uses: docker/build-push-action@v2 47 | with: 48 | context: . 49 | file: docker/Dockerfile 50 | platforms: | 51 | linux/amd64 52 | linux/arm64 53 | push: true 54 | tags: | 55 | ${{ secrets.DOCKER_USERNAME }}/nas-tools:latest 56 | ${{ secrets.DOCKER_USERNAME }}/nas-tools:${{ env.app_version }} 57 | -------------------------------------------------------------------------------- /app/helper/progress_helper.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from app.utils.commons import singleton 4 | from app.utils.types import ProgressKey 5 | 6 | 7 | @singleton 8 | class ProgressHelper(object): 9 | _process_detail = {} 10 | 11 | def __init__(self): 12 | self._process_detail = {} 13 | 14 | def init_config(self): 15 | pass 16 | 17 | def __reset(self, ptype=ProgressKey.Search): 18 | if isinstance(ptype, Enum): 19 | ptype = ptype.value 20 | self._process_detail[ptype] = { 21 | "enable": False, 22 | "value": 0, 23 | "text": "请稍候..." 24 | } 25 | 26 | def start(self, ptype=ProgressKey.Search): 27 | self.__reset(ptype) 28 | if isinstance(ptype, Enum): 29 | ptype = ptype.value 30 | self._process_detail[ptype]['enable'] = True 31 | 32 | def end(self, ptype=ProgressKey.Search): 33 | if isinstance(ptype, Enum): 34 | ptype = ptype.value 35 | if not self._process_detail.get(ptype): 36 | return 37 | self._process_detail[ptype]['enable'] = False 38 | 39 | def update(self, value=None, text=None, ptype=ProgressKey.Search): 40 | if isinstance(ptype, Enum): 41 | ptype = ptype.value 42 | if not self._process_detail.get(ptype, {}).get('enable'): 43 | return 44 | if value: 45 | self._process_detail[ptype]['value'] = value 46 | if text: 47 | self._process_detail[ptype]['text'] = text 48 | 49 | def get_process(self, ptype=ProgressKey.Search): 50 | if isinstance(ptype, Enum): 51 | ptype = ptype.value 52 | return self._process_detail.get(ptype) 53 | -------------------------------------------------------------------------------- /diff.md: -------------------------------------------------------------------------------- 1 | ![logo-blue](https://user-images.githubusercontent.com/51039935/197520391-f35db354-6071-4c12-86ea-fc450f04bc85.png) 2 | # NAS媒体库管理工具 3 | 4 | 5 | [![GitHub stars](https://img.shields.io/github/stars/0xforee/nas-tools?style=plastic)](https://github.com/0xforee/nas-tools/stargazers) 6 | [![GitHub forks](https://img.shields.io/github/forks/0xforee/nas-tools?style=plastic)](https://github.com/0xforee/nas-tools/network/members) 7 | [![GitHub issues](https://img.shields.io/github/issues/0xforee/nas-tools?style=plastic)](https://github.com/0xforee/nas-tools/issues) 8 | [![GitHub license](https://img.shields.io/github/license/0xforee/nas-tools?style=plastic)](https://github.com/0xforee/nas-tools/blob/master/LICENSE.md) 9 | [![Docker pulls](https://img.shields.io/docker/pulls/0xforee/nas-tools?style=plastic)](https://hub.docker.com/r/0xforee/nas-tools) 10 | [![Platform](https://img.shields.io/badge/platform-amd64/arm64-pink?style=plastic)](https://hub.docker.com/r/0xforee/nas-tools) 11 | 12 | Docker:https://hub.docker.com/repository/docker/0xforee/nas-tools 13 | 14 | ## 和主线分支不同点 15 | 1. 取消了用户认证 16 | 2. 取消新手刷流限制 17 | 1. 刷流增加部分下载能力,可以灵活配置下载包的大小比例。(部分站点有规则禁止,严重可能封号,请慎用!!!) 18 | 2. 刷流增加限免到期检测能力,超过限免时间,自动降速为 1B/s,防止流量偷跑,默认为关。(目前只测试过 mteam 和 hdmayi,其他暂未可知) 19 | 3. 刷流界面增加信息展示 20 | * 增加展示已保种大小 21 | * 种子明细增加展示种子大小,限免过期时间 22 | 3. 依旧保留了 BT 能力和内置 BT 站点,可以继续索引和下载 BT 磁链和种子文件 23 | 4. 依旧支持 jackett 和 prowlarr 索引器 24 | 5. 一些入口增加快捷跳转能力,方便将 nastools 作为媒体管理主要入口。具体如下: 25 | * 下载管理-正在下载:增加跳转下载器查看种子详情能力 26 | * 我的媒体库:增加标题跳转媒体库能力 27 | * 索引器:jackett 和 prowlarr 增加跳转各服务能力 28 | * 媒体服务器:每个服务器配置界面增加跳转服务能力 29 | 6. 支持 Mteam 新架构 30 | * 目前自测使用 ok,但是仍然可能有些场景没有测试到,我会尽快修正 31 | * 架构层面不支持 api key 的方式,所以仍旧使用 cookies 方式认证(请及时关注官方信息来判断这种方式是否合法) 32 | 33 | -------------------------------------------------------------------------------- /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/conf/systemconfig.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from app.helper import DictHelper 4 | from app.utils.commons import singleton 5 | from app.utils.types import SystemConfigKey 6 | 7 | 8 | @singleton 9 | class SystemConfig: 10 | # 系统设置 11 | systemconfig = {} 12 | 13 | def __init__(self): 14 | self.dicthelper = DictHelper() 15 | self.init_config() 16 | 17 | def init_config(self): 18 | """ 19 | 缓存系统设置 20 | """ 21 | for item in self.dicthelper.list("SystemConfig"): 22 | if not item: 23 | continue 24 | if self.__is_obj(item.VALUE): 25 | self.systemconfig[item.KEY] = json.loads(item.VALUE) 26 | else: 27 | self.systemconfig[item.KEY] = item.VALUE 28 | 29 | @staticmethod 30 | def __is_obj(obj): 31 | if isinstance(obj, list) or isinstance(obj, dict): 32 | return True 33 | else: 34 | return str(obj).startswith("{") or str(obj).startswith("[") 35 | 36 | def set_system_config(self, key: [SystemConfigKey, str], value): 37 | """ 38 | 设置系统设置 39 | """ 40 | if isinstance(key, SystemConfigKey): 41 | key = key.value 42 | # 更新内存 43 | self.systemconfig[key] = value 44 | # 写入数据库 45 | if self.__is_obj(value): 46 | if value is not None: 47 | value = json.dumps(value) 48 | else: 49 | value = '' 50 | self.dicthelper.set("SystemConfig", key, value) 51 | 52 | def get_system_config(self, key: [SystemConfigKey, str] = None): 53 | """ 54 | 获取系统设置 55 | """ 56 | if not key: 57 | return self.systemconfig 58 | if isinstance(key, SystemConfigKey): 59 | key = key.value 60 | return self.systemconfig.get(key) 61 | -------------------------------------------------------------------------------- /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: 5px; 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/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/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 | -------------------------------------------------------------------------------- /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 and res.status_code == 200: 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 | elif res is not None: 50 | return False, f"错误码:{res.status_code},错误原因:{res.reason}" 51 | else: 52 | return False, "未获取到返回信息" 53 | except Exception as msg_e: 54 | ExceptionUtils.exception_traceback(msg_e) 55 | return False, str(msg_e) 56 | 57 | def send_list_msg(self, **kwargs): 58 | pass 59 | -------------------------------------------------------------------------------- /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 and res.status_code == 200: 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 | elif res is not None: 50 | return False, f"错误码:{res.status_code},错误原因:{res.reason}" 51 | else: 52 | return False, "未获取到返回信息" 53 | except Exception as msg_e: 54 | ExceptionUtils.exception_traceback(msg_e) 55 | return False, str(msg_e) 56 | 57 | def send_list_msg(self, **kwargs): 58 | pass 59 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /package/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 | -------------------------------------------------------------------------------- /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/utils/ip_utils.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | import socket 3 | from urllib.parse import urlparse 4 | 5 | 6 | class IpUtils: 7 | 8 | @staticmethod 9 | def is_ipv4(ip): 10 | """ 11 | 判断是不是ipv4 12 | """ 13 | try: 14 | socket.inet_pton(socket.AF_INET, ip) 15 | except AttributeError: # no inet_pton here,sorry 16 | try: 17 | socket.inet_aton(ip) 18 | except socket.error: 19 | return False 20 | return ip.count('.') == 3 21 | except socket.error: # not a valid ip 22 | return False 23 | return True 24 | 25 | @staticmethod 26 | def is_ipv6(ip): 27 | """ 28 | 判断是不是ipv6 29 | """ 30 | try: 31 | socket.inet_pton(socket.AF_INET6, ip) 32 | except socket.error: # not a valid ip 33 | return False 34 | return True 35 | 36 | @staticmethod 37 | def is_internal(hostname): 38 | """ 39 | 判断一个host是内网还是外网 40 | """ 41 | hostname = urlparse(hostname).hostname 42 | if IpUtils.is_ip(hostname): 43 | return IpUtils.is_private_ip(hostname) 44 | else: 45 | return IpUtils.is_internal_domain(hostname) 46 | 47 | @staticmethod 48 | def is_ip(addr): 49 | """ 50 | 判断是不是ip 51 | """ 52 | try: 53 | socket.inet_aton(addr) 54 | return True 55 | except socket.error: 56 | return False 57 | 58 | @staticmethod 59 | def is_internal_domain(domain): 60 | """ 61 | 判断域名是否为内部域名 62 | """ 63 | # 获取域名对应的 IP 地址 64 | try: 65 | ip = socket.gethostbyname(domain) 66 | except socket.error: 67 | return False 68 | 69 | # 判断 IP 地址是否属于内网 IP 地址范围 70 | return IpUtils.is_private_ip(ip) 71 | 72 | @staticmethod 73 | def is_private_ip(ip_str): 74 | """ 75 | 判断是不是内网ip 76 | """ 77 | try: 78 | return ipaddress.ip_address(ip_str.strip()).is_private 79 | except Exception as e: 80 | print(e) 81 | return False 82 | -------------------------------------------------------------------------------- /app/helper/site_helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | import os 4 | import re 5 | 6 | from lxml import etree 7 | 8 | from app.utils import SystemUtils 9 | from config import RMT_SUBEXT 10 | 11 | 12 | class SiteHelper: 13 | 14 | @classmethod 15 | def is_logged_in(cls, html_text): 16 | """ 17 | 判断站点是否已经登陆 18 | :param html_text: 19 | :return: 20 | """ 21 | html = etree.HTML(html_text) 22 | if not html: 23 | return False 24 | # 存在明显的密码输入框,说明未登录 25 | if html.xpath("//input[@type='password']"): 26 | return False 27 | # 是否存在登出和用户面板等链接 28 | logout_or_usercp = html.xpath('//a[contains(@href, "logout") or contains(@data-url, "logout")' 29 | ' or contains(@href, "mybonus") ' 30 | ' or contains(@onclick, "logout") or contains(@href, "usercp")]') 31 | 32 | if logout_or_usercp: 33 | return True 34 | 35 | user_info_div = html.xpath('//div[@class="user-info-side"]') 36 | if user_info_div: 37 | return True 38 | 39 | return False 40 | 41 | @staticmethod 42 | def get_url_subtitle_name(disposition, url): 43 | """ 44 | 从站点下载请求中获取字幕文件名 45 | """ 46 | fname = re.findall(r"filename=\"?(.+)\"?", disposition or "") 47 | if fname: 48 | fname = str(fname[0].encode('ISO-8859-1').decode()).split(";")[0].strip() 49 | if fname.endswith('"'): 50 | fname = fname[:-1] 51 | elif url and os.path.splitext(url)[-1] in (RMT_SUBEXT + ['.zip']): 52 | fname = url.split("/")[-1] 53 | else: 54 | fname = str(datetime.now()) 55 | return fname 56 | 57 | @staticmethod 58 | def transfer_subtitle(source_sub_file, media_file): 59 | """ 60 | 转移站点字幕 61 | """ 62 | new_sub_file = "%s%s" % (os.path.splitext(media_file)[0], os.path.splitext(source_sub_file)[-1]) 63 | if os.path.exists(new_sub_file): 64 | return 1 65 | else: 66 | return SystemUtils.copy(source_sub_file, new_sub_file) 67 | -------------------------------------------------------------------------------- /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/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 | _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._token = self._client_config.get('token') 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 | 发送Chanify消息 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._token: 43 | return False, "参数未配置" 44 | sc_url = "%s/v1/sender/%s" % (self._server, self._token) 45 | params = parse.parse_qs(self._params or '') 46 | data = {key: value[0] for key, value in params.items()} 47 | data.update({'title': title, 'text': text}) 48 | # 发送文本 49 | res = RequestUtils().post_res(sc_url, data=parse.urlencode(data).encode()) 50 | if res and res.status_code == 200: 51 | return True, "发送成功" 52 | elif res is not None: 53 | return False, f"错误码:{res.status_code},错误原因:{res.reason}" 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NAS媒体库管理工具 2 | 3 | 4 | [![GitHub stars](https://img.shields.io/github/stars/0xforee/nas-tools?style=plastic)](https://github.com/0xforee/nas-tools/stargazers) 5 | [![GitHub forks](https://img.shields.io/github/forks/0xforee/nas-tools?style=plastic)](https://github.com/0xforee/nas-tools/network/members) 6 | [![GitHub issues](https://img.shields.io/github/issues/0xforee/nas-tools?style=plastic)](https://github.com/0xforee/nas-tools/issues) 7 | [![GitHub license](https://img.shields.io/github/license/0xforee/nas-tools?style=plastic)](https://github.com/0xforee/nas-tools/blob/master/LICENSE.md) 8 | [![Docker pulls](https://img.shields.io/docker/pulls/0xforee/nas-tools?style=plastic)](https://hub.docker.com/r/0xforee/nas-tools) 9 | [![Platform](https://img.shields.io/badge/platform-amd64/arm64-pink?style=plastic)](https://hub.docker.com/r/0xforee/nas-tools) 10 | 11 | Docker:https://hub.docker.com/repository/docker/0xforee/nas-tools 12 | 13 | API: http://localhost:3000/api/v1/ 14 | 15 | ## 写在开头 16 | 1. 感谢原作者提供的工具。 17 | 1. 这个项目是在自己使用过程中发现的一些改进点而诞生的,欢迎大家交流。 18 | 4. 每次版本更新内容请看 Releases 发布页面。 19 | 5. 喜欢请点个星吧,我会加快更新进度,感谢~ 20 | 21 | 22 | ## 功能: 23 | 24 | NAS媒体库管理工具。 25 | 26 | ## 基于主线做的改动方面 27 | 1. 用户认证优化 28 | 2. 新手刷流优化 29 | 1. 刷流任务优化: 30 | * 增加部分下载能力(拆包) 31 | * 增加限免到期检测能力 32 | * 刷流界面增加详细信息展示 33 | 3. 支持 BT 能力和内置 BT 站点,可以继续索引和下载 BT 磁链和种子文件 34 | 4. 支持 jackett 和 prowlarr 索引器 35 | 5. 增加一些入口的快捷跳转能力 36 | 6. 支持 Mteam 新架构 37 | 38 | 详细参考 [这里](diff.md)。 39 | 40 | ## 安装 41 | ### 1、Docker 42 | ``` 43 | docker pull 0xforee/nas-tools:latest 44 | ``` 45 | 教程见 [这里](docker/readme.md) 。 46 | 47 | 如无法连接Github,注意不要开启自动更新开关(NASTOOL_AUTO_UPDATE=false),将NASTOOL_CN_UPDATE设置为true可使用国内源加速安装依赖。 48 | 49 | ### 2、本地运行 50 | python3.10版本,需要预安装cython,如发现缺少依赖包需额外安装: 51 | ``` 52 | git clone -b master https://github.com/0xforee/nas-tools --recurse-submodule 53 | python3 -m pip install -r requirements.txt 54 | export NASTOOL_CONFIG="/xxx/config/config.yaml" 55 | nohup python3 run.py & 56 | ``` 57 | 58 | 59 | ## 免责声明 60 | 1) 本软件不提供任何内容,仅作为辅助工具简化用户手工操作,对用户的行为及内容毫不知情,使用本软件产生的任何责任需由使用者本人承担。 61 | 2) 本软件代码开源,基于开源代码进行修改,人为去除相关限制导致软件被分发、传播并造成责任事件的,需由代码修改发布者承担全部责任。同时按AGPL-3.0开源协议要求,基于此软件代码的所有修改必须开源。 62 | 3) 所有搜索结果均来自源站,本软件不承担任何责任 63 | 3) 本软件仅供学习交流,请保持低调,勿公开传播 64 | -------------------------------------------------------------------------------- /docker/dev.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | RUN apk update \ 3 | && apk add --no-cache libffi-dev \ 4 | && apk add --no-cache $(echo $(wget --no-check-certificate -qO- https://raw.githubusercontent.com/NAStool/nas-tools/dev/package_list.txt)) \ 5 | && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \ 6 | && echo "${TZ}" > /etc/timezone \ 7 | && ln -sf /usr/bin/python3 /usr/bin/python \ 8 | && curl https://rclone.org/install.sh | bash \ 9 | && if [ "$(uname -m)" = "x86_64" ]; then ARCH=amd64; elif [ "$(uname -m)" = "aarch64" ]; then ARCH=arm64; fi \ 10 | && curl https://dl.min.io/client/mc/release/linux-${ARCH}/mc --create-dirs -o /usr/bin/mc \ 11 | && chmod +x /usr/bin/mc \ 12 | && pip install --upgrade pip setuptools wheel \ 13 | && pip install cython \ 14 | && pip install -r https://raw.githubusercontent.com/NAStool/nas-tools/dev/requirements.txt \ 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/NAStool/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 | NT_HOME="/nt" 32 | WORKDIR ${WORKDIR} 33 | RUN mkdir ${NT_HOME} \ 34 | && addgroup -S nt -g 911 \ 35 | && adduser -S nt -G nt -h ${NT_HOME} -s /bin/bash -u 911 \ 36 | && python_ver=$(python3 -V | awk '{print $2}') \ 37 | && echo "${WORKDIR}/" > /usr/lib/python${python_ver%.*}/site-packages/nas-tools.pth \ 38 | && echo 'fs.inotify.max_user_watches=5242880' >> /etc/sysctl.conf \ 39 | && echo 'fs.inotify.max_user_instances=5242880' >> /etc/sysctl.conf \ 40 | && echo "nt ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers \ 41 | && git config --global pull.ff only \ 42 | && git clone -b dev ${REPO_URL} ${WORKDIR} --depth=1 --recurse-submodule \ 43 | && git config --global --add safe.directory ${WORKDIR} \ 44 | && chmod +x ${WORKDIR}/docker/entrypoint.sh 45 | EXPOSE 3000 46 | VOLUME ["/config"] 47 | ENTRYPOINT ["/nas-tools/docker/entrypoint.sh"] 48 | -------------------------------------------------------------------------------- /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 | keyword: { attribute: "keyword" }, 11 | person_list: { type: Array }, 12 | }; 13 | 14 | constructor() { 15 | super(); 16 | this.person_list = []; 17 | } 18 | 19 | // 仅执行一次 界面首次刷新后 20 | firstUpdated() { 21 | Golbal.get_cache_or_ajax("media_person", this.media_type, { tmdbid: this.tmdbid, type: this.media_type, keyword: this.keyword }, 22 | (ret) => { 23 | if (ret.code === 0) { 24 | this.person_list = ret.data; 25 | } 26 | } 27 | ); 28 | } 29 | 30 | render() { 31 | return html` 32 |
33 | 41 |
42 |
43 |
44 |
45 | ${this.person_list.length != 0 46 | ? this.person_list.map((item, index) => ( html` 47 | { 53 | navmenu("recommend?type="+this.media_type+"&subtype=person&personid="+item.id+"&title=参演作品&subtitle="+item.name) 54 | }} 55 | > 56 | ` ) ) 57 | : Array(20).fill(html``) 58 | } 59 |
60 |
61 |
62 | `; 63 | } 64 | 65 | } 66 | 67 | 68 | window.customElements.define("page-person", PagePerson); -------------------------------------------------------------------------------- /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 and res.status_code == 200: 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 | elif res is not None: 57 | return False, f"错误码:{res.status_code},错误原因:{res.reason}" 58 | else: 59 | return False, "未获取到返回信息" 60 | except Exception as msg_e: 61 | ExceptionUtils.exception_traceback(msg_e) 62 | return False, str(msg_e) 63 | 64 | def send_list_msg(self, **kwargs): 65 | pass 66 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | def get_login_wallpaper(time_now=datetime.datetime.now()): 11 | """ 12 | 获取Base64编码的壁纸图片 13 | """ 14 | wallpaper = Config().get_config('app').get('wallpaper') 15 | tmdbkey = Config().get_config('app').get('rmt_tmdbkey') 16 | if (not wallpaper or wallpaper == "themoviedb") and tmdbkey: 17 | # 每小时更新 18 | curr_time = datetime.datetime.strftime(time_now, '%Y%m%d%H') 19 | img_url, img_title, img_link = __get_themoviedb_wallpaper(curr_time) 20 | else: 21 | # 每天更新 22 | today = datetime.datetime.strftime(time_now, '%Y%m%d') 23 | img_url, img_title, img_link = __get_bing_wallpaper(today) 24 | img_enc = __get_image_b64(img_url) 25 | if img_enc: 26 | return img_enc, img_title, img_link 27 | return "", "", "" 28 | 29 | 30 | @lru_cache(maxsize=1) 31 | def __get_image_b64(img_url, cache_tag=None): 32 | """ 33 | 根据图片URL缓存 34 | 如果遇到同一地址返回随机图片的情况, 需要视情况传递cache_tag参数 35 | """ 36 | if img_url: 37 | res = RequestUtils().get_res(img_url) 38 | if res and res.status_code == 200: 39 | return base64.b64encode(res.content).decode() 40 | return "" 41 | 42 | 43 | @lru_cache(maxsize=1) 44 | def __get_themoviedb_wallpaper(cache_tag): 45 | """ 46 | 获取TheMovieDb的随机背景图 47 | cache_tag 缓存标记, 相同时会命中缓存 48 | """ 49 | return Media().get_random_discover_backdrop() 50 | 51 | 52 | @lru_cache(maxsize=1) 53 | def __get_bing_wallpaper(today): 54 | """ 55 | 获取Bing每日壁纸 56 | """ 57 | url = "https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&today=%s" % today 58 | try: 59 | resp = RequestUtils(timeout=5).get_res(url) 60 | except Exception as err: 61 | ExceptionUtils.exception_traceback(err) 62 | return "" 63 | if resp and resp.status_code == 200: 64 | if resp.json(): 65 | for image in resp.json().get('images') or []: 66 | img_url = f"https://cn.bing.com{image.get('url')}" if 'url' in image else '' 67 | img_title = image.get('title', '') 68 | img_link = image.get('copyrightlink', '') 69 | return img_url, img_title, img_link 70 | return '', '', '' 71 | -------------------------------------------------------------------------------- /web/static/js/tabler/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 /etc/timezone \ 7 | && ln -sf /usr/bin/python3 /usr/bin/python \ 8 | && curl https://rclone.org/install.sh | bash \ 9 | && if [ "$(uname -m)" = "x86_64" ]; then ARCH=amd64; elif [ "$(uname -m)" = "aarch64" ]; then ARCH=arm64; fi \ 10 | && curl https://dl.minio.org.cn/client/mc/release/linux-${ARCH}/mc --create-dirs -o /usr/bin/mc \ 11 | && chmod +x /usr/bin/mc \ 12 | && pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --upgrade pip setuptools wheel \ 13 | && pip install -i https://pypi.tuna.tsinghua.edu.cn/simple cython \ 14 | && pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r https://raw.githubusercontent.com/0xforee/nas-tools/master/requirements.txt \ 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=false \ 21 | NASTOOL_CN_UPDATE=true \ 22 | NASTOOL_VERSION=master \ 23 | PS1="\u@\h:\w \$ " \ 24 | REPO_URL="https://github.com/0xforee/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 | NT_HOME="/nt" 32 | WORKDIR ${WORKDIR} 33 | RUN mkdir ${NT_HOME} \ 34 | && addgroup -S nt -g 911 \ 35 | && adduser -S nt -G nt -h ${NT_HOME} -s /bin/bash -u 911 \ 36 | && python_ver=$(python3 -V | awk '{print $2}') \ 37 | && echo "${WORKDIR}/" > /usr/lib/python${python_ver%.*}/site-packages/nas-tools.pth \ 38 | && echo 'fs.inotify.max_user_watches=5242880' >> /etc/sysctl.conf \ 39 | && echo 'fs.inotify.max_user_instances=5242880' >> /etc/sysctl.conf \ 40 | && echo "nt ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers \ 41 | && git config --global pull.ff only \ 42 | && git clone -b master ${REPO_URL} ${WORKDIR} --depth=1 --recurse-submodule \ 43 | && git config --global --add safe.directory ${WORKDIR} \ 44 | && chmod +x ${WORKDIR}/docker/entrypoint.sh 45 | EXPOSE 3000 46 | VOLUME ["/config"] 47 | ENTRYPOINT ["/nas-tools/docker/entrypoint.sh"] 48 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | # 索引器ID 11 | # 索引器ID 12 | client_id = "prowlarr" 13 | # 索引器类型 14 | client_type = IndexerType.PROWLARR 15 | # 索引器名称 16 | client_name = IndexerType.PROWLARR.value 17 | _client_config = {} 18 | 19 | def __init__(self, config=None): 20 | super().__init__() 21 | if config: 22 | self._client_config = config 23 | else: 24 | self._client_config = Config().get_config('prowlarr') 25 | self.init_config() 26 | 27 | def init_config(self): 28 | if self._client_config: 29 | self.api_key = self._client_config.get('api_key') 30 | self.host = self._client_config.get('host') 31 | if self.host: 32 | if not self.host.startswith('http'): 33 | self.host = "http://" + self.host 34 | if not self.host.endswith('/'): 35 | self.host = self.host + "/" 36 | 37 | @classmethod 38 | def match(cls, ctype): 39 | return True if ctype in [cls.client_id, cls.client_type, cls.client_name] else False 40 | 41 | def get_status(self): 42 | """ 43 | 检查连通性 44 | :return: True、False 45 | """ 46 | if not self.api_key or not self.host: 47 | return False 48 | return True if self.get_indexers() else False 49 | 50 | def get_indexers(self): 51 | """ 52 | 获取配置的prowlarr indexer 53 | :return: indexer 信息 [(indexerId, indexerName, url)] 54 | """ 55 | indexer_query_url = f"{self.host}api/v1/indexerstats?apikey={self.api_key}" 56 | try: 57 | ret = RequestUtils().get_res(indexer_query_url) 58 | except Exception as e2: 59 | ExceptionUtils.exception_traceback(e2) 60 | return [] 61 | if not ret: 62 | return [] 63 | indexers = ret.json().get("indexers", []) 64 | return [IndexerConf({"id": v["indexerId"], 65 | "name": v["indexerName"], 66 | "domain": f'{self.host}{v["indexerId"]}/api', 67 | "builtin": False}) 68 | for v in indexers] 69 | 70 | def search(self, *kwargs): 71 | return super().search(*kwargs) 72 | -------------------------------------------------------------------------------- /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/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 is not None: 63 | return False, f"错误码:{res.status_code},错误原因:{res.reason}" 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/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 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.9.4 2 | aniso8601==9.0.1 3 | APScheduler==3.10.0 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 | cachetools==5.3.0 14 | certifi==2022.6.15 15 | cffi==1.15.1 16 | charset-normalizer==2.1.1 17 | click==8.1.3 18 | cn2an==0.5.17 19 | colorama==0.4.4 20 | colored==1.3.93 21 | cryptography==39.0.1 22 | cssselect==1.1.0 23 | DBUtils==3.0.2 24 | dateparser==1.1.4 25 | decorator==5.1.1 26 | executing==1.1.0 27 | Flask==2.2.3 28 | Flask-Login==0.6.2 29 | fast-bencode==1.1.3 30 | flask-compress==1.13 31 | flask-restx==1.0.6 32 | func_timeout==4.3.5 33 | greenlet==1.1.3.post0 34 | h11==0.12.0 35 | humanize==4.4.0 36 | ics==0.7.2 37 | idna==3.3 38 | influxdb==5.3.1 39 | itsdangerous==2.1.2 40 | iso639==0.1.4 41 | jedi==0.18.1 42 | Jinja2==3.1.2 43 | jsonpath==0.82 44 | jsonschema==4.16.0 45 | loguru==0.6.0 46 | lxml==4.9.1 47 | Mako==1.2.3 48 | MarkupSafe==2.1.1 49 | matplotlib-inline==0.1.6 50 | msgpack==1.0.4 51 | openai==0.27.2 52 | outcome==1.2.0 53 | parse==1.19.0 54 | parsel==1.6.0 55 | parso==0.8.3 56 | pexpect==4.8.0 57 | pickleshare==0.7.5 58 | proces==0.1.2 59 | prompt-toolkit==3.0.31 60 | psutil==5.9.4 61 | ptyprocess==0.7.0 62 | pure-eval==0.2.2 63 | pycparser==2.21 64 | pycryptodome==3.15.0 65 | Pygments==2.13.0 66 | PyJWT==2.6.0 67 | pymongo==4.2.0 68 | PyMySQL==1.0.2 69 | pyperclip==1.8.2 70 | pypushdeer==0.0.3 71 | pyquery==1.4.3 72 | pyrsistent==0.18.1 73 | PySocks==1.7.1 74 | python-dateutil==2.8.2 75 | python-dotenv==0.20.0 76 | python_hosts==1.0.3 77 | pytz==2022.2.1 78 | pytz-deprecation-shim==0.1.0.post0 79 | PyVirtualDisplay==3.0 80 | redis==3.5.3 81 | redis-py-cluster==2.1.3 82 | regex==2022.9.13 83 | requests==2.28.1 84 | ruamel.yaml==0.17.21 85 | ruamel.yaml.clib==0.2.7 86 | selenium==4.4.3 87 | six==1.16.0 88 | slack-sdk==3.19.5 89 | sniffio==1.2.0 90 | sortedcontainers==2.4.0 91 | soupsieve==2.3.2.post1 92 | SQLAlchemy==2.0.4 93 | stack-data==0.5.1 94 | srt==3.5.2 95 | terminal-layout==2.1.2 96 | tqdm==4.64.0 97 | traitlets==5.4.0 98 | trio==0.21.0 99 | trio-websocket==0.9.2 100 | typing_extensions==4.3.0 101 | tzdata==2022.2 102 | tzlocal==4.2 103 | undetected-chromedriver==3.1.7 104 | urllib3==1.26.12 105 | w3lib==2.0.1 106 | watchdog==2.1.9 107 | wcwidth==0.2.5 108 | webdriver-manager==3.8.5 109 | websockets==10.3 110 | Werkzeug==2.2.3 111 | wsproto==1.2.0 112 | zhconv==1.4.3 -------------------------------------------------------------------------------- /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 and res.status_code == 200: 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 | elif res is not None: 68 | return False, f"错误码:{res.status_code},错误原因:{res.reason}" 69 | else: 70 | return False, "未获取到返回信息" 71 | except Exception as msg_e: 72 | ExceptionUtils.exception_traceback(msg_e) 73 | return False, str(msg_e) 74 | 75 | def send_list_msg(self, **kwargs): 76 | pass 77 | -------------------------------------------------------------------------------- /web/static/js/modules/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 -------------------------------------------------------------------------------- /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: 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 | 81 | def list(self, dtype): 82 | """ 83 | 查询字典列表 84 | """ 85 | if not dtype: 86 | return [] 87 | return self._db.query(SYSTEMDICT).filter(SYSTEMDICT.TYPE == dtype).all() 88 | -------------------------------------------------------------------------------- /app/plugins/modules/libraryrefresh.py: -------------------------------------------------------------------------------- 1 | from app.mediaserver import MediaServer 2 | from app.plugins import EventHandler 3 | from app.plugins.modules._base import _IPluginModule 4 | from app.utils.types import EventType 5 | 6 | 7 | class LibraryRefresh(_IPluginModule): 8 | # 插件名称 9 | module_name = "实时刷新媒体库" 10 | # 插件描述 11 | module_desc = "入库完成后实时刷新媒体库服务器海报墙。" 12 | # 插件图标 13 | module_icon = "refresh.png" 14 | # 主题色 15 | module_color = "#32BEA6" 16 | # 插件版本 17 | module_version = "1.0" 18 | # 插件作者 19 | module_author = "jxxghp" 20 | # 作者主页 21 | author_url = "https://github.com/jxxghp" 22 | # 插件配置项ID前缀 23 | module_config_prefix = "libraryrefresh_" 24 | # 加载顺序 25 | module_order = 8 26 | # 可使用的用户级别 27 | auth_level = 2 28 | 29 | # 私有属性 30 | _enable = False 31 | 32 | mediaserver = None 33 | 34 | def init_config(self, config: dict = None): 35 | self.mediaserver = MediaServer() 36 | if config: 37 | self._enable = config.get("enable") 38 | 39 | def get_state(self): 40 | return self._enable 41 | 42 | @staticmethod 43 | def get_fields(): 44 | return [ 45 | # 同一板块 46 | { 47 | 'type': 'div', 48 | 'content': [ 49 | # 同一行 50 | [ 51 | { 52 | 'title': '开启媒体库实时刷新', 53 | 'required': "", 54 | 'tooltip': 'Emby已有电视剧新增剧集时只会刷新对应电视剧,其它场景下如开启了二级分类则只刷新二级分类对应媒体库,否则刷新整库;Jellyfin/Plex只支持刷新整库', 55 | 'type': 'switch', 56 | 'id': 'enable', 57 | } 58 | ], 59 | ] 60 | } 61 | ] 62 | 63 | def stop_service(self): 64 | pass 65 | 66 | @EventHandler.register(EventType.TransferFinished) 67 | def refresh(self, event): 68 | """ 69 | 监听入库完成事件 70 | """ 71 | if not self._enable: 72 | return 73 | event_data = event.event_data 74 | media_info = event_data.get("media_info") 75 | title = media_info.get("title") 76 | year = media_info.get("year") 77 | media_name = f"{title} ({year})" if year else title 78 | mediaserver_type = self.mediaserver.get_type().value 79 | self.info(f"媒体服务器 {mediaserver_type} 刷新媒体 {media_name}") 80 | self.mediaserver.refresh_library_by_items([{ 81 | "title": title, 82 | "year": year, 83 | "type": media_info.get("type"), 84 | "category": media_info.get("category"), 85 | "target_path": event_data.get("dest") 86 | }]) 87 | -------------------------------------------------------------------------------- /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 True, [] 32 | self.__get_token() 33 | if not self._token: 34 | log.warn(f"【INDEXER】{indexer.name} 未获取到token,无法搜索") 35 | return True, [] 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 True, [] 63 | else: 64 | log.warn(f"【INDEXER】{indexer.name} 搜索失败,无法连接 torrentapi.org") 65 | return True, [] 66 | return False, torrents 67 | -------------------------------------------------------------------------------- /app/plugins/modules/customreleasegroups.py: -------------------------------------------------------------------------------- 1 | from app.media.meta.release_groups import ReleaseGroupsMatcher 2 | from app.plugins.modules._base import _IPluginModule 3 | 4 | 5 | class CustomReleaseGroups(_IPluginModule): 6 | # 插件名称 7 | module_name = "自定义制作组/字幕组" 8 | # 插件描述 9 | module_desc = "添加无法识别的制作组/字幕组。" 10 | # 插件图标 11 | module_icon = "teamwork.png" 12 | # 主题色 13 | module_color = "#00ADEF" 14 | # 插件版本 15 | module_version = "1.0" 16 | # 插件作者 17 | module_author = "Shurelol" 18 | # 作者主页 19 | author_url = "https://github.com/Shurelol" 20 | # 插件配置项ID前缀 21 | module_config_prefix = "customreleasegroups_" 22 | # 加载顺序 23 | module_order = 6 24 | # 可使用的用户级别 25 | auth_level = 1 26 | 27 | # 私有属性 28 | _custom_release_groups = None 29 | _release_groups_matcher = None 30 | 31 | @staticmethod 32 | def get_fields(): 33 | return [ 34 | # 同一板块 35 | { 36 | 'type': 'div', 37 | 'content': [ 38 | # 同一行 39 | [ 40 | { 41 | 'title': '', 42 | 'required': '', 43 | 'tooltip': '', 44 | 'type': 'textarea', 45 | 'content': 46 | { 47 | 'id': 'release_groups', 48 | 'placeholder': '多个制作组/字幕组请用;分隔,支持正则表达式,特殊字符注意转义', 49 | 'rows': 5 50 | } 51 | } 52 | ] 53 | ] 54 | } 55 | ] 56 | 57 | def init_config(self, config=None): 58 | self._release_groups_matcher = ReleaseGroupsMatcher() 59 | 60 | # 读取配置 61 | if config: 62 | custom_release_groups = config.get('release_groups') 63 | if custom_release_groups: 64 | if custom_release_groups.startswith(';'): 65 | custom_release_groups = custom_release_groups[1:] 66 | if custom_release_groups.endswith(';'): 67 | custom_release_groups = custom_release_groups[:-1] 68 | custom_release_groups = custom_release_groups.replace(";", "|").replace("\n", "|") 69 | if custom_release_groups: 70 | self._release_groups_matcher.update_custom(custom_release_groups) 71 | self._custom_release_groups = custom_release_groups 72 | self.info("自定义制作组/字幕组已加载") 73 | 74 | def get_state(self): 75 | return True if self._custom_release_groups else False 76 | 77 | def stop_service(self): 78 | """ 79 | 退出插件 80 | """ 81 | pass 82 | -------------------------------------------------------------------------------- /docker/readme.md: -------------------------------------------------------------------------------- 1 | ## 特点 2 | 3 | - 基于alpine实现,镜像体积小; 4 | 5 | - 镜像层数少; 6 | 7 | - 支持 amd64/arm64 架构; 8 | 9 | - 重启即可更新程序,如果依赖有变化,会自动尝试重新安装依赖,若依赖自动安装不成功,会提示更新镜像; 10 | 11 | - 可以以非root用户执行任务,降低程序权限和潜在风险; 12 | 13 | - 可以设置文件掩码权限umask。 14 | 15 | ## 创建 16 | 17 | **注意** 18 | 19 | - 媒体目录的设置必须符合 [配置说明](https://github.com/0xforee/nas-tools#%E9%85%8D%E7%BD%AE) 的要求。 20 | 21 | - umask含义详见:http://www.01happy.com/linux-umask-analyze 。 22 | 23 | - 创建后请根据 [配置说明](https://github.com/0xforee/nas-tools#%E9%85%8D%E7%BD%AE) 及该文件本身的注释,修改`config/config.yaml`,修改好后再重启容器,最后访问`http://:`。 24 | 25 | **docker cli** 26 | 27 | ``` 28 | docker run -d \ 29 | --name nas-tools \ 30 | --hostname nas-tools \ 31 | -p 3000:3000 `# 默认的webui控制端口` \ 32 | -v $(pwd)/config:/config `# 冒号左边请修改为你想在主机上保存配置文件的路径` \ 33 | -v /你的媒体目录:/你想设置的容器内能见到的目录 `# 媒体目录,多个目录需要分别映射进来` \ 34 | -e PUID=0 `# 想切换为哪个用户来运行程序,该用户的uid,详见下方说明` \ 35 | -e PGID=0 `# 想切换为哪个用户来运行程序,该用户的gid,详见下方说明` \ 36 | -e UMASK=000 `# 掩码权限,默认000,可以考虑设置为022` \ 37 | -e NASTOOL_AUTO_UPDATE=false `# 如需在启动容器时自动升级程程序请设置为true` \ 38 | -e NASTOOL_CN_UPDATE=false `# 如果开启了容器启动自动升级程序,并且网络不太友好时,可以设置为true,会使用国内源进行软件更新` \ 39 | 0xforee/nas-tools 40 | ``` 41 | 42 | 如果你访问github的网络不太好,可以考虑在创建容器时增加设置一个环境变量`-e REPO_URL="https://ghproxy.com/https://github.com/0xforee/nas-tools.git" \`。 43 | 44 | **docker-compose** 45 | 46 | 新建`docker-compose.yaml`文件如下,并以命令`docker-compose up -d`启动。 47 | 48 | ``` 49 | version: "3" 50 | services: 51 | nas-tools: 52 | image: 0xforee/nas-tools:latest 53 | ports: 54 | - 3000:3000 # 默认的webui控制端口 55 | volumes: 56 | - ./config:/config # 冒号左边请修改为你想保存配置的路径 57 | - /你的媒体目录:/你想设置的容器内能见到的目录 # 媒体目录,多个目录需要分别映射进来,需要满足配置文件说明中的要求 58 | environment: 59 | - PUID=0 # 想切换为哪个用户来运行程序,该用户的uid 60 | - PGID=0 # 想切换为哪个用户来运行程序,该用户的gid 61 | - UMASK=000 # 掩码权限,默认000,可以考虑设置为022 62 | - NASTOOL_AUTO_UPDATE=false # 如需在启动容器时自动升级程程序请设置为true 63 | - NASTOOL_CN_UPDATE=false # 如果开启了容器启动自动升级程序,并且网络不太友好时,可以设置为true,会使用国内源进行软件更新 64 | #- REPO_URL=https://ghproxy.com/https://github.com/0xforee/nas-tools.git # 当你访问github网络很差时,可以考虑解释本行注释 65 | restart: always 66 | network_mode: bridge 67 | hostname: nas-tools 68 | container_name: nas-tools 69 | ``` 70 | 71 | ## 后续如何更新 72 | 73 | - 正常情况下,如果设置了`NASTOOL_AUTO_UPDATE=true`,重启容器即可自动更新nas-tools程序。 74 | 75 | - 设置了`NASTOOL_AUTO_UPDATE=true`时,如果启动时的日志提醒你 "更新失败,继续使用旧的程序来启动...",请再重启一次,如果一直都报此错误,请改善你的网络。 76 | 77 | - 设置了`NASTOOL_AUTO_UPDATE=true`时,如果启动时的日志提醒你 "无法安装依赖,请更新镜像...",则需要删除旧容器,删除旧镜像,重新pull镜像,再重新创建容器。 78 | 79 | ## 关于PUID/PGID的说明 80 | 81 | - 如在使用诸如emby、jellyfin、plex、qbittorrent、transmission、deluge、jackett、sonarr、radarr等等的docker镜像,请保证创建本容器时的PUID/PGID和它们一样。 82 | 83 | - 在docker宿主上,登陆媒体文件所有者的这个用户,然后分别输入`id -u`和`id -g`可获取到uid和gid,分别设置为PUID和PGID即可。 84 | 85 | - `PUID=0` `PGID=0`指root用户,它拥有最高权限,若你的媒体文件的所有者不是root,不建议设置为`PUID=0` `PGID=0`。 86 | 87 | ## 如果要硬连接如何映射 88 | 89 | 参考下图,由imogel@telegram制作。 90 | 91 | ![如何映射](volume.png) 92 | -------------------------------------------------------------------------------- /web/static/js/ace/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 | -------------------------------------------------------------------------------- /app/plugins/event_manager.py: -------------------------------------------------------------------------------- 1 | from queue import Queue, Empty 2 | 3 | import log 4 | from app.utils.commons import singleton 5 | from app.utils.types import EventType 6 | 7 | 8 | @singleton 9 | class EventManager: 10 | """ 11 | 事件管理器 12 | """ 13 | 14 | # 事件队列 15 | _eventQueue = None 16 | # 事件响应函数字典 17 | _handlers = {} 18 | 19 | def __init__(self): 20 | self.init_config() 21 | 22 | def init_config(self): 23 | # 事件队列 24 | self._eventQueue = Queue() 25 | # 事件响应函数字典 26 | self._handlers = {} 27 | 28 | def get_event(self): 29 | """ 30 | 获取事件 31 | """ 32 | try: 33 | event = self._eventQueue.get(block=True, timeout=1) 34 | handlerList = self._handlers.get(event.event_type) 35 | return event, handlerList or [] 36 | except Empty: 37 | return None, [] 38 | 39 | def add_event_listener(self, etype: EventType, handler): 40 | """ 41 | 注册事件处理 42 | """ 43 | try: 44 | handlerList = self._handlers[etype.value] 45 | except KeyError: 46 | handlerList = [] 47 | self._handlers[etype.value] = handlerList 48 | if handler not in handlerList: 49 | handlerList.append(handler) 50 | log.debug(f"已注册事件:{etype.value}{handler}") 51 | 52 | def remove_event_listener(self, etype: EventType, handler): 53 | """ 54 | 移除监听器的处理函数 55 | """ 56 | try: 57 | handlerList = self._handlers[etype.value] 58 | if handler in handlerList[:]: 59 | handlerList.remove(handler) 60 | if not handlerList: 61 | del self._handlers[etype.value] 62 | except KeyError: 63 | pass 64 | 65 | def send_event(self, etype: EventType, data: dict = None): 66 | """ 67 | 发送事件 68 | """ 69 | if etype not in EventType: 70 | return 71 | event = Event(etype.value) 72 | event.event_data = data or {} 73 | log.debug(f"发送事件:{etype.value} - {event.event_data}") 74 | self._eventQueue.put(event) 75 | 76 | def register(self, etype: [EventType, list]): 77 | """ 78 | 事件注册 79 | :param etype: 事件类型 80 | """ 81 | 82 | def decorator(f): 83 | if isinstance(etype, list): 84 | for et in etype: 85 | self.add_event_listener(et, f) 86 | else: 87 | self.add_event_listener(etype, f) 88 | return f 89 | 90 | return decorator 91 | 92 | 93 | class Event(object): 94 | """ 95 | 事件对象 96 | """ 97 | 98 | def __init__(self, event_type=None): 99 | # 事件类型 100 | self.event_type = event_type 101 | # 字典用于保存具体的事件数据 102 | self.event_data = {} 103 | 104 | 105 | # 实例引用,用于注册事件 106 | EventHandler = EventManager() 107 | -------------------------------------------------------------------------------- /app/plugins/modules/_base.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | import log 4 | from app.conf import SystemConfig 5 | from app.message import Message 6 | 7 | class _IPluginModule(metaclass=ABCMeta): 8 | """ 9 | 插件模块基类 10 | """ 11 | # 插件名称 12 | module_name = "" 13 | # 插件描述 14 | module_desc = "" 15 | # 插件图标 16 | module_icon = "" 17 | # 主题色 18 | module_color = "" 19 | # 插件版本 20 | module_version = "1.0" 21 | # 插件作者 22 | module_author = "" 23 | # 作者主页 24 | author_url = "" 25 | # 插件配置项ID前缀:为了避免各插件配置表单相冲突,配置表单元素ID自动在前面加上此前缀 26 | module_config_prefix = "plugin_" 27 | # 显示顺序 28 | module_order = 0 29 | # 可使用的用户级别 30 | auth_level = 1 31 | 32 | @staticmethod 33 | @abstractmethod 34 | def get_fields(): 35 | """ 36 | 获取配置字典,用于生成表单 37 | """ 38 | pass 39 | 40 | @abstractmethod 41 | def get_state(self): 42 | """ 43 | 获取插件启用状态 44 | """ 45 | pass 46 | 47 | @abstractmethod 48 | def init_config(self, config: dict): 49 | """ 50 | 生效配置信息 51 | :param config: 配置信息字典 52 | """ 53 | pass 54 | 55 | @abstractmethod 56 | def stop_service(self): 57 | """ 58 | 停止插件 59 | """ 60 | pass 61 | 62 | def update_config(self, config: dict, plugin_id=None): 63 | """ 64 | 更新配置信息 65 | :param config: 配置信息字典 66 | :param plugin_id: 插件ID 67 | """ 68 | if not plugin_id: 69 | plugin_id = self.__class__.__name__ 70 | return SystemConfig().set_system_config("plugin.%s" % plugin_id, config) 71 | 72 | def get_config(self, plugin_id=None): 73 | """ 74 | 获取配置信息 75 | :param plugin_id: 插件ID 76 | """ 77 | if not plugin_id: 78 | plugin_id = self.__class__.__name__ 79 | return SystemConfig().get_system_config("plugin.%s" % plugin_id) 80 | 81 | @staticmethod 82 | def send_message(title, text=None, image=None): 83 | """ 84 | 发送消息 85 | """ 86 | return Message().send_plugin_message(title=title, 87 | text=text, 88 | image=image) 89 | 90 | def info(self, msg): 91 | """ 92 | 记录INFO日志 93 | :param msg: 日志信息 94 | """ 95 | log.info(f"【Plugin】{self.module_name} - {msg}") 96 | 97 | def warn(self, msg): 98 | """ 99 | 记录插件WARN日志 100 | :param msg: 日志信息 101 | """ 102 | log.warn(f"【Plugin】{self.module_name} - {msg}") 103 | 104 | def error(self, msg): 105 | """ 106 | 记录插件ERROR日志 107 | :param msg: 日志信息 108 | """ 109 | log.error(f"【Plugin】{self.module_name} - {msg}") 110 | 111 | def debug(self, msg): 112 | """ 113 | 记录插件Debug日志 114 | :param msg: 日志信息 115 | """ 116 | log.debug(f"【Plugin】{self.module_name} - {msg}") 117 | -------------------------------------------------------------------------------- /app/utils/mteam_utils.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | 3 | from app.utils import RequestUtils 4 | from config import Config 5 | 6 | 7 | class MteamUtils: 8 | @staticmethod 9 | def get_mteam_torrent_info(torrent_url, cookie, ua=None, proxy=False): 10 | api = "%s/api/torrent/detail" 11 | from urllib.parse import urlparse 12 | parse_result = urlparse(torrent_url) 13 | api = api % (str(parse_result.scheme) + "://" + str(parse_result.hostname)) 14 | torrent_id = torrent_url.split('/')[-1] 15 | req = MteamUtils.buildRequestUtils(cookie, ua, proxy).post_res(url=api, params={"id": torrent_id}) 16 | 17 | if req and req.status_code == 200: 18 | return req.json().get("data") 19 | 20 | return None 21 | 22 | @staticmethod 23 | def mteam_sign(site_info): 24 | site_url = site_info.get("signurl") 25 | site_cookie = site_info.get("cookie") 26 | ua = site_info.get("ua") 27 | url = f"{site_url}api/member/profile" 28 | res = MteamUtils.buildRequestUtils( 29 | headers=ua, 30 | cookies=site_cookie, 31 | proxies=Config().get_proxies() if site_info.get("proxy") else None, 32 | timeout=15 33 | ).post_res(url=url) 34 | if res and res.status_code == 200: 35 | user_info = res.json() 36 | if user_info and user_info.get("data"): 37 | return True, "连接成功" 38 | return False, "Cookie已失效" 39 | 40 | @staticmethod 41 | def get_mteam_url(url, cookie=None, ua=None, referer=None, proxy=False): 42 | if url.find('api/rss/dl') != -1: 43 | return url 44 | api = "%s/api/torrent/genDlToken" 45 | parse_result = urlparse(url) 46 | api = api % (str(parse_result.scheme) + "://" + str(parse_result.hostname)) 47 | torrent_id = url.split('/')[-1] 48 | req = MteamUtils.buildRequestUtils( 49 | headers=ua, 50 | cookies=cookie, 51 | referer=referer, 52 | proxies=Config().get_proxies() if proxy else None 53 | ).post_res(url=api, params={"id": torrent_id}) 54 | 55 | if req and req.status_code == 200: 56 | return req.json().get("data") 57 | 58 | return None 59 | 60 | @staticmethod 61 | def buildRequestUtils(cookies, headers=None, proxies=False, content_type=None, accept_type=None, session=None, referer=None, timeout=30): 62 | if cookies.find("=") != -1: 63 | return RequestUtils(headers=headers, cookies=cookies, timeout=timeout, referer=referer, 64 | content_type=content_type, session=session, accept_type=accept_type, 65 | proxies=Config().get_proxies() if proxies else None) 66 | else: 67 | # use api key 68 | return RequestUtils(headers=headers, api_key=cookies, timeout=timeout, referer=referer, 69 | content_type=content_type, session=session, accept_type=accept_type, 70 | proxies=Config().get_proxies() if proxies else None) 71 | -------------------------------------------------------------------------------- /app/helper/ffmpeg_helper.py: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | 4 | from app.utils import SystemUtils 5 | 6 | 7 | class FfmpegHelper: 8 | 9 | @staticmethod 10 | def get_thumb_image_from_video(video_path, image_path, frames="00:03:01"): 11 | """ 12 | 使用ffmpeg从视频文件中截取缩略图 13 | """ 14 | if not video_path or not image_path: 15 | return False 16 | cmd = 'ffmpeg -i "{video_path}" -ss {frames} -vframes 1 -f image2 "{image_path}"'.format(video_path=video_path, 17 | frames=frames, 18 | image_path=image_path) 19 | result = SystemUtils.execute(cmd) 20 | if result: 21 | return True 22 | return False 23 | 24 | @staticmethod 25 | def extract_wav_from_video(video_path, audio_path, audio_index=None): 26 | """ 27 | 使用ffmpeg从视频文件中提取16000hz, 16-bit的wav格式音频 28 | """ 29 | if not video_path or not audio_path: 30 | return False 31 | 32 | # 提取指定音频流 33 | if audio_index: 34 | command = ['ffmpeg', "-hide_banner", "-loglevel", "warning", '-y', '-i', video_path, 35 | '-map', f'0:a:{audio_index}', 36 | '-acodec', 'pcm_s16le', '-ac', '1', '-ar', '16000', audio_path] 37 | else: 38 | command = ['ffmpeg', "-hide_banner", "-loglevel", "warning", '-y', '-i', video_path, 39 | '-acodec', 'pcm_s16le', '-ac', '1', '-ar', '16000', audio_path] 40 | 41 | ret = subprocess.run(command).returncode 42 | if ret == 0: 43 | return True 44 | return False 45 | 46 | @staticmethod 47 | def get_video_metadata(video_path): 48 | """ 49 | 获取视频元数据 50 | """ 51 | if not video_path: 52 | return False 53 | 54 | try: 55 | command = ['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', video_path] 56 | result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 57 | if result.returncode == 0: 58 | return json.loads(result.stdout.decode("utf-8")) 59 | except Exception as e: 60 | print(e) 61 | return None 62 | 63 | @staticmethod 64 | def extract_subtitle_from_video(video_path, subtitle_path, subtitle_index=None): 65 | """ 66 | 从视频中提取字幕 67 | """ 68 | if not video_path or not subtitle_path: 69 | return False 70 | 71 | if subtitle_index: 72 | command = ['ffmpeg', "-hide_banner", "-loglevel", "warning", '-y', '-i', video_path, 73 | '-map', f'0:s:{subtitle_index}', 74 | subtitle_path] 75 | else: 76 | command = ['ffmpeg', "-hide_banner", "-loglevel", "warning", '-y', '-i', video_path, subtitle_path] 77 | ret = subprocess.run(command).returncode 78 | if ret == 0: 79 | return True 80 | return False 81 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 56 |
57 | 58 |
59 |
60 | 61 | -------------------------------------------------------------------------------- /web/static/components/cmd-dialog/cmd-action.js: -------------------------------------------------------------------------------- 1 | import {classMap, html, LitElement, nothing, repeat, unsafeCSS, unsafeHTML} from "../utility/lit-core.min.js"; 2 | import style from './style.js'; 3 | 4 | export class CmdAction extends LitElement { 5 | 6 | static styles = unsafeCSS(style); 7 | 8 | static properties = { 9 | theme: {attribute: "theme"}, 10 | action: {attribute: "action"}, 11 | selected: {attribute: "selected "}, 12 | }; 13 | 14 | constructor() { 15 | super(); 16 | this.selected = false 17 | this.theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 18 | this.addEventListener('click', this.click); 19 | } 20 | 21 | /** 22 | * Scroll to show element 23 | */ 24 | ensureInView() { 25 | requestAnimationFrame(() => { 26 | this.scrollIntoView({block: 'nearest'}); 27 | }); 28 | } 29 | 30 | /** 31 | * Click event 32 | */ 33 | click() { 34 | this.dispatchEvent( 35 | new CustomEvent('actionSelected', { 36 | detail: this.action, 37 | bubbles: true, 38 | composed: true, 39 | }), 40 | ); 41 | } 42 | 43 | /** 44 | * Updated 45 | * @param changedProperties 46 | */ 47 | updated(changedProperties) { 48 | if (changedProperties.has('selected') && this.selected) { 49 | this.ensureInView(); 50 | } 51 | } 52 | 53 | render() { 54 | const classes = { 55 | selected: this.selected, 56 | dark: this.theme === 'dark', 57 | }; 58 | 59 | return html` 60 |
  • 61 | ${this.img} 62 | 63 | ${this.action.title} 64 | ${this.description} 65 | 66 | ${this.hotkeys} 67 |
  • 68 | `; 69 | } 70 | 71 | /** 72 | * Get hotkeys 73 | * @private 74 | */ 75 | get hotkeys() { 76 | if (this.action?.hotkey) { 77 | const hotkeys = this.action.hotkey 78 | .replace('cmd', '⌘') 79 | .replace('shift', '⇧') 80 | .replace('alt', '⌥') 81 | .replace('ctrl', '⌃') 82 | .toUpperCase() 83 | .split('+'); 84 | return hotkeys.length > 0 ? html`${repeat(hotkeys, hotkey => html`${hotkey}`)}` : ''; 86 | } 87 | 88 | return nothing; 89 | } 90 | 91 | /** 92 | * Get description 93 | * @private 94 | */ 95 | get description() { 96 | return this.action.description ? html`${this.action.description}` : nothing; 97 | } 98 | 99 | /** 100 | * Get icon 101 | * @private 102 | */ 103 | get img() { 104 | return this.action.img ? html`${unsafeHTML(this.action.img)}` : nothing; 105 | } 106 | } 107 | 108 | window.customElements.define("cmd-action", CmdAction); -------------------------------------------------------------------------------- /app/plugins/modules/synctimer.py: -------------------------------------------------------------------------------- 1 | from apscheduler.schedulers.background import BackgroundScheduler 2 | from apscheduler.triggers.cron import CronTrigger 3 | 4 | from app.plugins.modules._base import _IPluginModule 5 | from app.sync import Sync 6 | from config import Config 7 | 8 | 9 | class SyncTimer(_IPluginModule): 10 | # 插件名称 11 | module_name = "定时目录同步" 12 | # 插件描述 13 | module_desc = "定时对同步目录进行整理。" 14 | # 插件图标 15 | module_icon = "synctimer.png" 16 | # 主题色 17 | module_color = "#53BA48" 18 | # 插件版本 19 | module_version = "1.0" 20 | # 插件作者 21 | module_author = "jxxghp" 22 | # 作者主页 23 | author_url = "https://github.com/jxxghp" 24 | # 插件配置项ID前缀 25 | module_config_prefix = "synctimer_" 26 | # 加载顺序 27 | module_order = 5 28 | # 可使用的用户级别 29 | user_level = 1 30 | 31 | # 私有属性 32 | _sync = None 33 | _scheduler = None 34 | # 限速开关 35 | _cron = None 36 | 37 | @staticmethod 38 | def get_fields(): 39 | return [ 40 | # 同一板块 41 | { 42 | 'type': 'div', 43 | 'content': [ 44 | # 同一行 45 | [ 46 | { 47 | 'title': '同步周期', 48 | 'required': "required", 49 | 'tooltip': '支持5位cron表达式;仅适用于挂载网盘或网络共享等目录同步监控无法正常工作的场景下使用,正常挂载本地目录无法同步的,应优先查看日志解决问题,留空则不启动', 50 | 'type': 'text', 51 | 'content': [ 52 | { 53 | 'id': 'cron', 54 | 'placeholder': '0 0 */2 * *', 55 | } 56 | ] 57 | } 58 | ] 59 | ] 60 | } 61 | ] 62 | 63 | def init_config(self, config=None): 64 | self._sync = Sync() 65 | 66 | # 读取配置 67 | if config: 68 | self._cron = config.get("cron") 69 | 70 | # 停止现有任务 71 | self.stop_service() 72 | 73 | # 启动定时任务 74 | if self._cron: 75 | self._scheduler = BackgroundScheduler(timezone=Config().get_timezone()) 76 | self._scheduler.add_job(func=self.__timersync, 77 | trigger=CronTrigger.from_crontab(self._cron)) 78 | self._scheduler.print_jobs() 79 | self._scheduler.start() 80 | self.info(f"目录定时同步服务启动,周期:{self._cron}") 81 | 82 | def get_state(self): 83 | return True if self._cron else False 84 | 85 | def __timersync(self): 86 | """ 87 | 开始同步 88 | """ 89 | self.info("开始定时同步 ...") 90 | self._sync.transfer_all_sync() 91 | self.info("定时同步完成") 92 | 93 | def stop_service(self): 94 | """ 95 | 退出插件 96 | """ 97 | try: 98 | if self._scheduler: 99 | self._scheduler.remove_all_jobs() 100 | if self._scheduler.running: 101 | self._scheduler.shutdown() 102 | self._scheduler = None 103 | except Exception as e: 104 | print(str(e)) 105 | -------------------------------------------------------------------------------- /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 | # 索引器ID 13 | client_id = "jackett" 14 | # 索引器类型 15 | client_type = IndexerType.JACKETT 16 | # 索引器名称 17 | client_name = IndexerType.JACKETT.value 18 | _client_config = {} 19 | _password = None 20 | 21 | def __init__(self, config=None): 22 | super().__init__() 23 | if config: 24 | self._client_config = config 25 | else: 26 | self._client_config = Config().get_config('jackett') 27 | self.init_config() 28 | 29 | def init_config(self): 30 | if self._client_config: 31 | self.api_key = self._client_config.get('api_key') 32 | self._password = self._client_config.get('password') 33 | self.host = self._client_config.get('host') 34 | if self.host: 35 | if not self.host.startswith('http'): 36 | self.host = "http://" + self.host 37 | if not self.host.endswith('/'): 38 | self.host = self.host + "/" 39 | 40 | def get_status(self): 41 | """ 42 | 检查连通性 43 | :return: True、False 44 | """ 45 | if not self.api_key or not self.host: 46 | return False 47 | return True if self.get_indexers() else False 48 | 49 | @classmethod 50 | def match(cls, ctype): 51 | return True if ctype in [cls.client_id, cls.client_type, cls.client_name] else False 52 | 53 | def get_type(self): 54 | return self.client_type 55 | 56 | def get_status(self): 57 | """ 58 | 检查连通性 59 | :return: True、False 60 | """ 61 | return True 62 | 63 | def get_indexers(self): 64 | """ 65 | 获取配置的jackett indexer 66 | :return: indexer 信息 [(indexerId, indexerName, url)] 67 | """ 68 | # 获取Cookie 69 | cookie = None 70 | session = requests.session() 71 | res = RequestUtils(session=session).post_res(url=f"{self.host}UI/Dashboard", 72 | data={"password": self._password}) 73 | if res and session.cookies: 74 | cookie = session.cookies.get_dict() 75 | indexer_query_url = f"{self.host}api/v2.0/indexers?configured=true" 76 | try: 77 | ret = RequestUtils(cookies=cookie).get_res(indexer_query_url) 78 | if not ret or not ret.json(): 79 | return [] 80 | return [IndexerConf({"id": v["id"], 81 | "name": v["name"], 82 | "domain": f'{self.host}api/v2.0/indexers/{v["id"]}/results/torznab/', 83 | "public": True if v['type'] == 'public' else False, 84 | "builtin": False}) 85 | for v in ret.json()] 86 | except Exception as e2: 87 | ExceptionUtils.exception_traceback(e2) 88 | return [] 89 | 90 | def search(self, *kwargs): 91 | return super().search(*kwargs) 92 | --------------------------------------------------------------------------------