├── 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 |
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 |
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 |
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 |
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 | 
2 | # NAS媒体库管理工具
3 |
4 |
5 | [](https://github.com/0xforee/nas-tools/stargazers)
6 | [](https://github.com/0xforee/nas-tools/network/members)
7 | [](https://github.com/0xforee/nas-tools/issues)
8 | [](https://github.com/0xforee/nas-tools/blob/master/LICENSE.md)
9 | [](https://hub.docker.com/r/0xforee/nas-tools)
10 | [](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 | [](https://github.com/0xforee/nas-tools/stargazers)
5 | [](https://github.com/0xforee/nas-tools/network/members)
6 | [](https://github.com/0xforee/nas-tools/issues)
7 | [](https://github.com/0xforee/nas-tools/blob/master/LICENSE.md)
8 | [](https://hub.docker.com/r/0xforee/nas-tools)
9 | [](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 |
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 | 
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 |
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 |
--------------------------------------------------------------------------------