├── .flaskenv
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ ├── config.yml
│ └── feature_request.yml
└── workflows
│ ├── build-beta.yml
│ ├── build-package.yml
│ └── build.yml
├── .gitignore
├── .gitmodules
├── LICENSE.md
├── README.md
├── app
├── __init__.py
├── apis
│ ├── __init__.py
│ └── mteam_api.py
├── brushtask.py
├── conf
│ ├── __init__.py
│ ├── moduleconf.py
│ └── systemconfig.py
├── db
│ ├── __init__.py
│ ├── main_db.py
│ ├── media_db.py
│ └── models.py
├── downloader
│ ├── __init__.py
│ ├── client
│ │ ├── __init__.py
│ │ ├── _base.py
│ │ ├── _pyaria2.py
│ │ ├── _pypan115.py
│ │ ├── aria2.py
│ │ ├── pan115.py
│ │ ├── pikpak.py
│ │ ├── qbittorrent.py
│ │ └── transmission.py
│ └── downloader.py
├── filetransfer.py
├── filter.py
├── helper
│ ├── __init__.py
│ ├── chrome_helper.py
│ ├── cloudflare_helper.py
│ ├── db_helper.py
│ ├── dict_helper.py
│ ├── display_helper.py
│ ├── ffmpeg_helper.py
│ ├── meta_helper.py
│ ├── ocr_helper.py
│ ├── openai_helper.py
│ ├── plugin_helper.py
│ ├── progress_helper.py
│ ├── redis_helper.py
│ ├── rss_helper.py
│ ├── security_helper.py
│ ├── site_helper.py
│ ├── submodule_helper.py
│ ├── thread_helper.py
│ └── words_helper.py
├── indexer
│ ├── __init__.py
│ ├── client
│ │ ├── __init__.py
│ │ ├── _base.py
│ │ ├── _mt_spider.py
│ │ ├── _plugins.py
│ │ ├── _render_spider.py
│ │ ├── _spider.py
│ │ ├── _tnode.py
│ │ ├── _torrentleech.py
│ │ └── builtin.py
│ ├── indexer.py
│ └── indexerConf.py
├── media
│ ├── __init__.py
│ ├── bangumi.py
│ ├── category.py
│ ├── douban.py
│ ├── doubanapi
│ │ ├── __init__.py
│ │ ├── apiv2.py
│ │ └── webapi.py
│ ├── fanart.py
│ ├── media.py
│ ├── meta
│ │ ├── __init__.py
│ │ ├── _base.py
│ │ ├── customization.py
│ │ ├── mediaItem.py
│ │ ├── metaanime.py
│ │ ├── metainfo.py
│ │ ├── metavideo.py
│ │ ├── metavideov2.py
│ │ └── release_groups.py
│ ├── scraper.py
│ └── tmdbv3api
│ │ ├── __init__.py
│ │ ├── as_obj.py
│ │ ├── exceptions.py
│ │ ├── objs
│ │ ├── __init__.py
│ │ ├── discover.py
│ │ ├── episode.py
│ │ ├── find.py
│ │ ├── genre.py
│ │ ├── movie.py
│ │ ├── person.py
│ │ ├── search.py
│ │ ├── trending.py
│ │ └── tv.py
│ │ └── tmdb.py
├── mediaserver
│ ├── __init__.py
│ ├── client
│ │ ├── __init__.py
│ │ ├── _base.py
│ │ ├── emby.py
│ │ ├── jellyfin.py
│ │ └── plex.py
│ └── media_server.py
├── message
│ ├── __init__.py
│ ├── client
│ │ ├── __init__.py
│ │ ├── _base.py
│ │ ├── bark.py
│ │ ├── chanify.py
│ │ ├── gotify.py
│ │ ├── iyuu.py
│ │ ├── ntfy.py
│ │ ├── pushdeer.py
│ │ ├── pushplus.py
│ │ ├── serverchan.py
│ │ ├── slack.py
│ │ ├── synologychat.py
│ │ ├── telegram.py
│ │ ├── webhook.py
│ │ └── wechat.py
│ ├── message.py
│ └── message_center.py
├── plugins
│ ├── __init__.py
│ ├── event_manager.py
│ ├── modules
│ │ ├── __init__.py
│ │ ├── _autosignin
│ │ │ ├── 52pt.py
│ │ │ ├── __init__.py
│ │ │ ├── _base.py
│ │ │ ├── btschool.py
│ │ │ ├── carpt.py
│ │ │ ├── chdbits.py
│ │ │ ├── haidan.py
│ │ │ ├── hares.py
│ │ │ ├── hdarea.py
│ │ │ ├── hdchina.py
│ │ │ ├── hdcity.py
│ │ │ ├── hdfans.py
│ │ │ ├── hdsky.py
│ │ │ ├── hdtime.py
│ │ │ ├── hdupt.py
│ │ │ ├── hhanclub.py
│ │ │ ├── opencd.py
│ │ │ ├── pterclub.py
│ │ │ ├── pttime.py
│ │ │ ├── tjupt.py
│ │ │ ├── ttg.py
│ │ │ ├── u2.py
│ │ │ └── zhuque.py
│ │ ├── _base.py
│ │ ├── autobackup.py
│ │ ├── autosignin.py
│ │ ├── autosub.py
│ │ ├── chinesesubfinder.py
│ │ ├── cloudflarespeedtest.py
│ │ ├── cookiecloud.py
│ │ ├── customhosts.py
│ │ ├── customization.py
│ │ ├── customreleasegroups.py
│ │ ├── diskspacesaver.py
│ │ ├── doubanrank.py
│ │ ├── doubansync.py
│ │ ├── downloader_helper.py
│ │ ├── iyuu
│ │ │ ├── __init__.py
│ │ │ └── iyuu_helper.py
│ │ ├── iyuuautoseed.py
│ │ ├── jackett.py
│ │ ├── libraryrefresh.py
│ │ ├── libraryscraper.py
│ │ ├── media_library_archive.py
│ │ ├── mediasyncdel.py
│ │ ├── movielike.py
│ │ ├── movierandom.py
│ │ ├── opensubtitles.py
│ │ ├── prowlarr.py
│ │ ├── speedlimiter.py
│ │ ├── synctimer.py
│ │ ├── torrentmark.py
│ │ ├── torrentremover.py
│ │ ├── torrenttransfer.py
│ │ └── webhook.py
│ └── plugin_manager.py
├── rss.py
├── rsschecker.py
├── scheduler.py
├── searcher.py
├── sites
│ ├── __init__.py
│ ├── site_cookie.py
│ ├── site_limiter.py
│ ├── site_subtitle.py
│ ├── site_userinfo.py
│ ├── siteconf.py
│ ├── sites.py
│ └── siteuserinfo
│ │ ├── __init__.py
│ │ ├── _base.py
│ │ ├── discuz.py
│ │ ├── file_list.py
│ │ ├── gazelle.py
│ │ ├── ipt_project.py
│ │ ├── mteam_torrent.py
│ │ ├── nexus_php.py
│ │ ├── nexus_project.py
│ │ ├── nexus_rabbit.py
│ │ ├── small_horse.py
│ │ ├── tnode.py
│ │ ├── torrent_leech.py
│ │ └── unit3d.py
├── subscribe.py
├── sync.py
├── torrentremover.py
└── utils
│ ├── __init__.py
│ ├── cache_manager.py
│ ├── commons.py
│ ├── dom_utils.py
│ ├── episode_format.py
│ ├── exception_utils.py
│ ├── http_utils.py
│ ├── image_utils.py
│ ├── ip_utils.py
│ ├── json_utils.py
│ ├── nfo_reader.py
│ ├── number_utils.py
│ ├── path_utils.py
│ ├── rsstitle_utils.py
│ ├── scheduler_utils.py
│ ├── string_utils.py
│ ├── system_utils.py
│ ├── tokens.py
│ ├── torrent.py
│ └── types.py
├── config.py
├── config
├── config.yaml
└── default-category.yaml
├── dbscript_gen.py
├── docker
├── Dockerfile
├── beta.Dockerfile
├── compose.yml
├── debian-beta.Dockerfile
├── debian.Dockerfile
├── readme.md
├── rootfs
│ └── etc
│ │ └── s6-overlay
│ │ └── s6-rc.d
│ │ ├── init-010-update
│ │ ├── run
│ │ ├── type
│ │ └── up
│ │ ├── init-020-fixuser
│ │ ├── dependencies.d
│ │ │ └── init-010-update
│ │ ├── run
│ │ ├── type
│ │ └── up
│ │ ├── svc-nastools
│ │ ├── dependencies.d
│ │ │ └── init-020-fixuser
│ │ ├── finish
│ │ ├── notification-fd
│ │ ├── run
│ │ └── type
│ │ ├── svc-redis
│ │ ├── dependencies.d
│ │ │ └── init-020-fixuser
│ │ ├── finish
│ │ ├── notification-fd
│ │ ├── run
│ │ └── type
│ │ └── user
│ │ └── contents.d
│ │ ├── init-010-update
│ │ ├── init-020-fixuser
│ │ ├── svc-nastools
│ │ └── svc-redis
└── volume.png
├── initializer.py
├── log.py
├── package
├── builder
│ ├── Dockerfile
│ └── alpine.Dockerfile
├── nas-tools.ico
├── nas-tools.spec
├── rely
│ ├── hook-cn2an.py
│ ├── hook-iso639.py
│ ├── hook-zhconv.py
│ ├── template.jinja2
│ └── upx.exe
├── requirements.txt
└── trayicon.py
├── package_list.txt
├── package_list_debian.txt
├── requirements.txt
├── run.py
├── scripts
├── env.py
├── script.py.mako
├── sqls
│ ├── init_filter.sql
│ ├── init_userrss_v3.sql
│ ├── stop_all_service.sql
│ ├── update_downloader.sql
│ ├── update_subscribe.sql
│ ├── update_systemdict.sql
│ ├── update_userpris.sql
│ └── update_userrss.sql
└── versions
│ ├── 13a58bd5311f_1_2_2.py
│ ├── 1f5cc26cdd3d_1_2_3.py
│ ├── 69508d1aed24_1_2_1.py
│ ├── 6abeaa9ece15_1_2_0.py.py
│ ├── 702b7666a634_1_2_5.py
│ ├── 7c14267ffbe4_1_2_8.py
│ ├── a19a48dbb41b_1_2_7.py
│ ├── ae61cfa6ada6_1_2_4.py
│ ├── d68a85a8f10d_1_2_6.py
│ ├── eb3437042cc8_1_3_1.py
│ └── ff1b04a637f8_1_3_0.py
├── tests
├── __init__.py
├── cases
│ ├── __init__.py
│ └── meta_cases.py
├── playground.py
├── run.py
├── test_metainfo.py
└── tests_utils.py
├── third_party.txt
├── third_party
└── feapder
│ ├── .gitignore
│ ├── LICENSE
│ ├── MANIFEST.in
│ ├── README.md
│ ├── feapder
│ ├── VERSION
│ ├── __init__.py
│ ├── buffer
│ │ ├── __init__.py
│ │ ├── item_buffer.py
│ │ └── request_buffer.py
│ ├── commands
│ │ ├── __init__.py
│ │ ├── cmdline.py
│ │ ├── create
│ │ │ ├── __init__.py
│ │ │ ├── create_cookies.py
│ │ │ ├── create_init.py
│ │ │ ├── create_item.py
│ │ │ ├── create_json.py
│ │ │ ├── create_params.py
│ │ │ ├── create_project.py
│ │ │ ├── create_setting.py
│ │ │ ├── create_spider.py
│ │ │ └── create_table.py
│ │ ├── create_builder.py
│ │ ├── retry.py
│ │ ├── shell.py
│ │ └── zip.py
│ ├── core
│ │ ├── __init__.py
│ │ ├── base_parser.py
│ │ ├── collector.py
│ │ ├── handle_failed_items.py
│ │ ├── handle_failed_requests.py
│ │ ├── parser_control.py
│ │ ├── scheduler.py
│ │ └── spiders
│ │ │ ├── __init__.py
│ │ │ ├── air_spider.py
│ │ │ ├── batch_spider.py
│ │ │ ├── spider.py
│ │ │ └── task_spider.py
│ ├── db
│ │ ├── __init__.py
│ │ ├── memorydb.py
│ │ ├── mongodb.py
│ │ ├── mysqldb.py
│ │ └── redisdb.py
│ ├── dedup
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── basefilter.py
│ │ ├── bitarray.py
│ │ ├── bloomfilter.py
│ │ ├── expirefilter.py
│ │ └── litefilter.py
│ ├── network
│ │ ├── __init__.py
│ │ ├── downloader
│ │ │ ├── __init__.py
│ │ │ ├── _requests.py
│ │ │ ├── _selenium.py
│ │ │ └── base.py
│ │ ├── item.py
│ │ ├── proxy_pool
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ └── proxy_pool.py
│ │ ├── proxy_pool_old.py
│ │ ├── request.py
│ │ ├── response.py
│ │ ├── selector.py
│ │ ├── user_agent.py
│ │ └── user_pool
│ │ │ ├── __init__.py
│ │ │ ├── base_user_pool.py
│ │ │ ├── gold_user_pool.py
│ │ │ ├── guest_user_pool.py
│ │ │ └── normal_user_pool.py
│ ├── pipelines
│ │ ├── __init__.py
│ │ ├── console_pipeline.py
│ │ ├── mongo_pipeline.py
│ │ └── mysql_pipeline.py
│ ├── requirements.txt
│ ├── setting.py
│ ├── templates
│ │ ├── air_spider_template.tmpl
│ │ ├── batch_spider_template.tmpl
│ │ ├── item_template.tmpl
│ │ ├── project_template
│ │ │ ├── CHECK_DATA.md
│ │ │ ├── README.md
│ │ │ ├── items
│ │ │ │ └── __init__.py
│ │ │ ├── main.py
│ │ │ ├── setting.py
│ │ │ └── spiders
│ │ │ │ └── __init__.py
│ │ ├── spider_template.tmpl
│ │ ├── task_spider_template.tmpl
│ │ └── update_item_template.tmpl
│ └── utils
│ │ ├── __init__.py
│ │ ├── custom_argparse.py
│ │ ├── email_sender.py
│ │ ├── js
│ │ ├── intercept.js
│ │ └── stealth.min.js
│ │ ├── log.py
│ │ ├── metrics.py
│ │ ├── perfect_dict.py
│ │ ├── redis_lock.py
│ │ ├── tools.py
│ │ └── webdriver
│ │ ├── __init__.py
│ │ ├── selenium_driver.py
│ │ ├── webdirver.py
│ │ └── webdriver_pool.py
│ └── setup.py
├── version.py
└── web
├── __init__.py
├── action.py
├── apiv1.py
├── backend
├── WXBizMsgCrypt3.py
├── __init__.py
├── pro_user.py
├── search_torrents.py
├── user.cp310-win_amd64.pyd
├── user.cpython-310-aarch64-linux-gnu.so
├── user.cpython-310-darwin.so
├── user.cpython-310-x86_64-linux-gnu.so
├── user.sites.bin
├── wallpaper.py
└── web_utils.py
├── main.py
├── robots.txt
├── security.py
├── static
├── components
│ ├── accordion
│ │ ├── index.js
│ │ └── seasons
│ │ │ └── index.js
│ ├── card
│ │ ├── index.js
│ │ ├── normal
│ │ │ ├── index.js
│ │ │ ├── placeholder.js
│ │ │ └── state.js
│ │ └── person
│ │ │ └── index.js
│ ├── cmd-dialog
│ │ ├── cmd-action.js
│ │ ├── index.js
│ │ └── style.js
│ ├── custom
│ │ ├── chips
│ │ │ └── index.html
│ │ ├── img
│ │ │ └── index.js
│ │ ├── index.js
│ │ ├── plex-library-img
│ │ │ └── index.js
│ │ └── slide
│ │ │ └── index.js
│ ├── index.js
│ ├── layout
│ │ ├── index.js
│ │ ├── navbar
│ │ │ ├── button.js
│ │ │ └── index.js
│ │ └── searchbar
│ │ │ └── index.js
│ ├── lit-index.js
│ ├── page
│ │ ├── discovery
│ │ │ └── index.js
│ │ ├── index.js
│ │ ├── mediainfo
│ │ │ └── index.js
│ │ └── person
│ │ │ └── index.js
│ ├── plugin
│ │ ├── index.js
│ │ └── modal
│ │ │ └── index.js
│ └── utility
│ │ ├── lit-core.min.js
│ │ ├── lit-state.js
│ │ └── utility.js
├── css
│ ├── demo.min.css
│ ├── dropzone.css
│ ├── fullcalendar.min.css
│ ├── jQueryFileTree.min.css
│ ├── nprogress.css
│ ├── style.css
│ ├── tabler-vendors.min.css
│ └── tabler.min.css
├── favicon.ico
├── img
│ ├── bug_fixing.svg
│ ├── downloader
│ │ ├── 115.jpg
│ │ ├── aria2.png
│ │ ├── pikpak.png
│ │ ├── qbittorrent.png
│ │ └── transmission.png
│ ├── filetree
│ │ ├── application.png
│ │ ├── code.png
│ │ ├── css.png
│ │ ├── db.png
│ │ ├── directory-lock.png
│ │ ├── directory.png
│ │ ├── doc.png
│ │ ├── file-lock.png
│ │ ├── file.png
│ │ ├── film.png
│ │ ├── flash.png
│ │ ├── folder_open.png
│ │ ├── html.png
│ │ ├── java.png
│ │ ├── linux.png
│ │ ├── music.png
│ │ ├── pdf.png
│ │ ├── php.png
│ │ ├── picture.png
│ │ ├── ppt.png
│ │ ├── psd.png
│ │ ├── ruby.png
│ │ ├── script.png
│ │ ├── spinner.gif
│ │ ├── txt.png
│ │ ├── xls.png
│ │ └── zip.png
│ ├── icon-imdb.png
│ ├── icons
│ │ ├── 1024.png
│ │ ├── 128.png
│ │ ├── 144.png
│ │ ├── 152.png
│ │ ├── 167.png
│ │ ├── 172.png
│ │ ├── 180.png
│ │ ├── 196.png
│ │ ├── 196_ALT.png
│ │ ├── 216.png
│ │ ├── 256.png
│ │ ├── 512.png
│ │ └── 512_ALT.png
│ ├── indexer
│ │ ├── indexer.jpg
│ │ ├── indexer.png
│ │ ├── jackett.png
│ │ └── prowlarr.png
│ ├── joyride.svg
│ ├── logo
│ │ ├── logo-16x16.png
│ │ ├── logo-32x32.png
│ │ ├── logo-black.png
│ │ ├── logo-blue.png
│ │ ├── logo-transparent.png
│ │ ├── logo-white.png
│ │ └── logo.png
│ ├── mediaserver
│ │ ├── emby.png
│ │ ├── emby_backdrop.png
│ │ ├── jellyfin.jpg
│ │ ├── jellyfin.png
│ │ ├── jellyfin_backdrop.jpg
│ │ ├── plex.png
│ │ └── plex_backdrop.png
│ ├── medicine.svg
│ ├── message
│ │ ├── bark.webp
│ │ ├── chanify.png
│ │ ├── gotify.png
│ │ ├── iyuu.png
│ │ ├── ntfy.webp
│ │ ├── pushdeer.png
│ │ ├── pushplus.jpg
│ │ ├── serverchan.png
│ │ ├── slack.png
│ │ ├── synologychat.png
│ │ ├── telegram.png
│ │ └── wechat.png
│ ├── mobile_application.svg
│ ├── movie.jpg
│ ├── music.png
│ ├── no-image.png
│ ├── no-image.svg
│ ├── person.png
│ ├── plugins
│ │ ├── SpeedLimiter.jpg
│ │ ├── autosubtitles.jpeg
│ │ ├── backup.png
│ │ ├── chinesesubfinder.png
│ │ ├── cloud.png
│ │ ├── cloudflare.jpg
│ │ ├── diskusage.jpg
│ │ ├── douban.png
│ │ ├── emby.png
│ │ ├── hosts.png
│ │ ├── iyuu.png
│ │ ├── jackett.png
│ │ ├── like.jpg
│ │ ├── mediasyncdel.png
│ │ ├── movie.jpg
│ │ ├── nfo.png
│ │ ├── opensubtitles.png
│ │ ├── prowlarr.png
│ │ ├── random.png
│ │ ├── refresh.png
│ │ ├── regex.png
│ │ ├── scraper.png
│ │ ├── signin.png
│ │ ├── synctimer.png
│ │ ├── tag.png
│ │ ├── teamwork.png
│ │ ├── torrentremover.png
│ │ ├── torrenttransfer.jpg
│ │ └── webhook.png
│ ├── posting_photo.svg
│ ├── printing_invoices.svg
│ ├── pt.jpg
│ ├── quitting_time.svg
│ ├── sign_in.svg
│ ├── sites
│ │ ├── 1ptba.ico
│ │ ├── acgrip.ico
│ │ ├── audiences.ico
│ │ ├── dmhy.ico
│ │ ├── eztv.ico
│ │ ├── freefarm.ico
│ │ ├── hddolby.ico
│ │ ├── hdfans.ico
│ │ ├── hhclub.ico
│ │ ├── icc2022.ico
│ │ ├── iyuu.png
│ │ ├── leaves.ico
│ │ ├── lemonhd.ico
│ │ ├── mikanani.ico
│ │ ├── nyaa.png
│ │ ├── piggo.ico
│ │ ├── rarbg.ico
│ │ ├── sharkpt.ico
│ │ ├── torrentgalaxy.png
│ │ ├── wintersakura.ico
│ │ └── zmpt.ico
│ ├── splash
│ │ ├── apple-splash-1125-2436.png
│ │ ├── apple-splash-1136-640.png
│ │ ├── apple-splash-1170-2532.png
│ │ ├── apple-splash-1242-2208.png
│ │ ├── apple-splash-1242-2688.png
│ │ ├── apple-splash-1284-2778.png
│ │ ├── apple-splash-1334-750.png
│ │ ├── apple-splash-1536-2048.png
│ │ ├── apple-splash-1620-2160.png
│ │ ├── apple-splash-1668-2224.png
│ │ ├── apple-splash-1668-2388.png
│ │ ├── apple-splash-1792-828.png
│ │ ├── apple-splash-2048-1536.png
│ │ ├── apple-splash-2048-2732.png
│ │ ├── apple-splash-2160-1620.png
│ │ ├── apple-splash-2208-1242.png
│ │ ├── apple-splash-2224-1668.png
│ │ ├── apple-splash-2388-1668.png
│ │ ├── apple-splash-2436-1125.png
│ │ ├── apple-splash-2532-1170.png
│ │ ├── apple-splash-2688-1242.png
│ │ ├── apple-splash-2732-2048.png
│ │ ├── apple-splash-2778-1284.png
│ │ ├── apple-splash-640-1136.png
│ │ ├── apple-splash-750-1334.png
│ │ └── apple-splash-828-1792.png
│ ├── startup.jpg
│ ├── tmdb.png
│ ├── tmdb.webp
│ ├── tv.png
│ ├── users.png
│ ├── webhook_icon.png
│ └── work_together.svg
├── js
│ ├── ace
│ │ ├── ace.js
│ │ ├── mode-css.js
│ │ ├── mode-javascript.js
│ │ ├── mode-json.js
│ │ ├── mode-yaml.js
│ │ ├── theme-one_dark.js
│ │ ├── theme-xcode.js
│ │ ├── worker-css.js
│ │ ├── worker-javascript.js
│ │ ├── worker-json.js
│ │ └── worker-yaml.js
│ ├── fullcalendar
│ │ ├── fullcalendar.min.js
│ │ └── locales
│ │ │ └── zh-cn.js
│ ├── functions.js
│ ├── jquery-3.3.1.min.js
│ ├── modules
│ │ ├── FileSaver.min.js
│ │ ├── callapp-lib.js
│ │ ├── dom-to-image.min.js
│ │ ├── echarts.min.js
│ │ ├── fuse.esm.min.js
│ │ ├── hotkeys.esm.js
│ │ ├── jQueryFileTree.min.js
│ │ ├── moment.min.js
│ │ ├── nprogress.js
│ │ ├── numeral.min.js
│ │ └── reconnecting-websocket.js
│ ├── tabler
│ │ ├── demo-theme.min.js
│ │ ├── demo.min.js
│ │ ├── libs
│ │ │ ├── dropzone-min.js
│ │ │ ├── list.min.js
│ │ │ └── tom-select.base.min.js
│ │ └── tabler.min.js
│ └── util.js
└── site.webmanifest
└── templates
├── 404.html
├── 500.html
├── discovery
├── mediainfo.html
├── person.html
├── ranking.html
└── recommend.html
├── download
├── downloading.html
└── torrent_remove.html
├── index.html
├── login.html
├── macro
├── form.html
├── head.html
├── oops.html
└── svg.html
├── navigation.html
├── openapp.html
├── rename
├── history.html
├── mediafile.html
├── tmdbcache.html
└── unidentification.html
├── rss
├── movie_rss.html
├── rss_calendar.html
├── rss_history.html
├── rss_parser.html
├── tv_rss.html
└── user_rss.html
├── search.html
├── service.html
├── setting
├── basic.html
├── customwords.html
├── directorysync.html
├── download_setting.html
├── downloader.html
├── filterrule.html
├── indexer.html
├── library.html
├── mediaserver.html
├── notification.html
├── plugin.html
└── users.html
├── site
├── brushtask.html
├── resources.html
├── site.html
├── sitelist.html
└── statistics.html
└── test.html
/.flaskenv:
--------------------------------------------------------------------------------
1 | FLASK_APP=web/main.py
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: 问题反馈
2 | description: File a bug report
3 | title: "[错误报告]: 请在此处简单描述你的问题"
4 | labels: ["bug"]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | 请确认以下信息:
10 | 1. 请按此模板提交issues,不按模板提交的问题将直接关闭。
11 | 2. 如果你的问题可以直接在以往 issue 中找到,那么你的 issue 将会被直接关闭。
12 | 3. 提交问题务必描述清楚、附上日志,描述不清导致无法理解和分析的问题会被直接关闭。
13 | - type: checkboxes
14 | id: ensure
15 | attributes:
16 | label: 确认
17 | description: 在提交 issue 之前,请确认你已经阅读并确认以下内容
18 | options:
19 | - label: 我的版本是最新版本,我的版本号与 [version](https://github.com/hsuyelin/nas-tools/releases/latest) 相同。
20 | required: true
21 | - label: 我已经 [issue](https://github.com/hsuyelin/nas-tools/issues) 中搜索过,确认我的问题没有被提出过。
22 | required: true
23 | - label: 我已经修改标题,将标题中的 描述 替换为我遇到的问题。
24 | required: true
25 | - type: input
26 | id: version
27 | attributes:
28 | label: 当前程序版本
29 | description: 遇到问题时程序所在的版本号
30 | validations:
31 | required: true
32 | - type: dropdown
33 | id: type
34 | attributes:
35 | label: 问题类型
36 | description: 你在以下哪个部分碰到了问题
37 | options:
38 | - 主程序运行问题
39 | - 插件问题
40 | - Docker或运行环境问题
41 | - 其他问题
42 | validations:
43 | required: true
44 | - type: textarea
45 | id: what-happened
46 | attributes:
47 | label: 问题描述
48 | description: 请详细描述你碰到的问题
49 | placeholder: "问题描述"
50 | validations:
51 | required: true
52 | - type: textarea
53 | id: logs
54 | attributes:
55 | label: 发生问题时系统日志和配置文件
56 | description: 问题出现时,程序运行日志请复制到这里。
57 | render: bash
58 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: 项目讨论
4 | url: https://github.com/hsuyelin/nas-tools/discussions/new/choose
5 | about: discussion
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: 功能改进
2 | description: Feature Request
3 | title: "[功能改进]: "
4 | labels: ["feature request"]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | 请说明你希望添加的功能。
10 | - type: input
11 | id: version
12 | attributes:
13 | label: 当前程序版本
14 | description: 目前使用的程序版本
15 | validations:
16 | required: true
17 | - type: textarea
18 | id: feature-request
19 | attributes:
20 | label: 功能改进
21 | description: 请详细描述需要改进或者添加的功能。
22 | placeholder: "功能改进"
23 | validations:
24 | required: true
25 | - type: textarea
26 | id: references
27 | attributes:
28 | label: 参考资料
29 | description: 可以列举一些参考资料,但是不要引用同类但商业化软件的任何内容。
30 | placeholder: "参考资料"
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/**
2 | *.pyc
3 | *.c
4 | /test.py
5 | /setup.py
6 | /build_sites.py
7 | /build/**
8 | /venv/**
9 | /cloudflarespeedtest/**
10 | .python-version
11 | .vscode/
12 | Dockerfile-dev
13 | config/sites.json
14 | nastools_test.py
15 | /node_modules/**
16 | package-lock.json
17 | package.json
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "third_party/feapder"]
2 | path = third_party/feapder
3 | url = https://github.com/jxxghp/feapder
4 |
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/app/__init__.py
--------------------------------------------------------------------------------
/app/apis/__init__.py:
--------------------------------------------------------------------------------
1 | from .mteam_api import MTeamApi
2 |
--------------------------------------------------------------------------------
/app/conf/__init__.py:
--------------------------------------------------------------------------------
1 | from .systemconfig import SystemConfig
2 | from .moduleconf import ModuleConf
3 |
--------------------------------------------------------------------------------
/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(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(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 |
--------------------------------------------------------------------------------
/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(), '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 |
--------------------------------------------------------------------------------
/app/downloader/__init__.py:
--------------------------------------------------------------------------------
1 | from .downloader import Downloader
2 |
--------------------------------------------------------------------------------
/app/downloader/client/__init__.py:
--------------------------------------------------------------------------------
1 | from .qbittorrent import Qbittorrent
2 | from .transmission import Transmission
3 |
--------------------------------------------------------------------------------
/app/helper/__init__.py:
--------------------------------------------------------------------------------
1 | from .chrome_helper import ChromeHelper, init_chrome
2 | from .meta_helper import MetaHelper
3 | from .progress_helper import ProgressHelper
4 | from .security_helper import SecurityHelper
5 | from .thread_helper import ThreadHelper
6 | from .db_helper import DbHelper
7 | from .dict_helper import DictHelper
8 | from .display_helper import DisplayHelper
9 | from .site_helper import SiteHelper
10 | from .ocr_helper import OcrHelper
11 | from .words_helper import WordsHelper
12 | from .submodule_helper import SubmoduleHelper
13 | from .ffmpeg_helper import FfmpegHelper
14 | from .redis_helper import RedisHelper
15 | from .rss_helper import RssHelper
16 | from .plugin_helper import PluginHelper
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/helper/plugin_helper.py:
--------------------------------------------------------------------------------
1 | from cachetools import cached, TTLCache
2 |
3 | from app.utils import RequestUtils
4 |
5 |
6 | # 2023年08月30日 nastool原作者服务已失效
7 | class PluginHelper:
8 |
9 | @staticmethod
10 | def install(plugin_id):
11 | """
12 | 插件安装统计计数
13 | """
14 | # return RequestUtils(timeout=5).get(f"https://nastool.org/plugin/{plugin_id}/install")
15 | pass
16 |
17 | @staticmethod
18 | def report(plugins):
19 | """
20 | 批量上报插件安装统计数据
21 | """
22 | # return RequestUtils(content_type="application/json",
23 | # timeout=5).post(f"https://nastool.org/plugin/update",
24 | # json={
25 | # "plugins": [
26 | # {
27 | # "plugin_id": plugin,
28 | # "count": 1
29 | # } for plugin in plugins
30 | # ]
31 | # })
32 | return {}
33 |
34 | @staticmethod
35 | @cached(cache=TTLCache(maxsize=1, ttl=3600))
36 | def statistic():
37 | """
38 | 获取插件安装统计数据
39 | """
40 | # ret = RequestUtils(accept_type="application/json",
41 | # timeout=5).get_res("https://nastool.org/plugin/statistic")
42 | # if ret:
43 | # try:
44 | # return ret.json()
45 | # except Exception as e:
46 | # print(e)
47 | # return {}
48 | return {}
49 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/helper/submodule_helper.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import importlib
3 | import pkgutil
4 |
5 |
6 | class SubmoduleHelper:
7 | @classmethod
8 | def import_submodules(cls, package, filter_func=lambda name, obj: True):
9 | """
10 | 导入子模块
11 | :param package: 父包名
12 | :param filter_func: 子模块过滤函数,入参为模块名和模块对象,返回True则导入,否则不导入
13 | :return:
14 | """
15 |
16 | submodules = []
17 | packages = importlib.import_module(package).__path__
18 | for importer, package_name, _ in pkgutil.iter_modules(packages):
19 | full_package_name = f'{package}.{package_name}'
20 | if full_package_name.startswith('_'):
21 | continue
22 | module = importlib.import_module(full_package_name)
23 | for name, obj in module.__dict__.items():
24 | if name.startswith('_'):
25 | continue
26 | if isinstance(obj, type) and filter_func(name, obj):
27 | submodules.append(obj)
28 |
29 | return submodules
30 |
--------------------------------------------------------------------------------
/app/helper/thread_helper.py:
--------------------------------------------------------------------------------
1 | from concurrent.futures import ThreadPoolExecutor
2 |
3 | from app.utils.commons import singleton
4 |
5 |
6 | @singleton
7 | class ThreadHelper:
8 | _thread_num = 100
9 | executor = None
10 |
11 | def __init__(self):
12 | self.executor = ThreadPoolExecutor(max_workers=self._thread_num)
13 |
14 | def init_config(self):
15 | pass
16 |
17 | def start_thread(self, func, kwargs):
18 | self.executor.submit(func, *kwargs)
19 |
--------------------------------------------------------------------------------
/app/indexer/__init__.py:
--------------------------------------------------------------------------------
1 | from .indexer import Indexer
2 |
--------------------------------------------------------------------------------
/app/indexer/client/__init__.py:
--------------------------------------------------------------------------------
1 | from .builtin import BuiltinIndexer
2 |
--------------------------------------------------------------------------------
/app/indexer/client/_plugins.py:
--------------------------------------------------------------------------------
1 |
2 | from app.plugins import PluginManager
3 | from config import Config
4 |
5 | class PluginsSpider(object):
6 |
7 | # 私有方法
8 | _level = 99
9 | _plugin = {}
10 | _proxy = None
11 | _indexer = None
12 |
13 | def __int__(self, indexer):
14 | self._indexer = indexer
15 | if indexer.proxy:
16 | self._proxy = Config().get_proxies()
17 | self._plugin = PluginManager().get_plugin_apps(self._level).get(self._indexer.parser)
18 |
19 | def status(self, indexer):
20 | try:
21 | plugin = PluginManager().get_plugin_apps(self._level).get(indexer.parser)
22 | return True if plugin else False
23 | except Exception as e:
24 | return False
25 |
26 | def search(self, keyword, indexer, page=0):
27 | try:
28 | result_array = PluginManager().run_plugin_method(pid=indexer.parser, method='search', keyword=keyword, indexer=indexer, page=page)
29 | if not result_array:
30 | return False, []
31 | return True, result_array
32 | except Exception as e:
33 | return False, []
34 |
35 | def sites(self):
36 | result = []
37 | try:
38 | plugins = PluginManager().get_plugin_apps(self._level)
39 | for key in plugins:
40 | if plugins.get(key)['installed']:
41 | result_array = PluginManager().run_plugin_method(pid=plugins.get(key)['id'], method='get_indexers')
42 | if result_array:
43 | result.extend(result_array)
44 | except Exception as e:
45 | pass
46 | return result
--------------------------------------------------------------------------------
/app/media/__init__.py:
--------------------------------------------------------------------------------
1 | from .category import Category
2 | from .media import Media
3 | from .scraper import Scraper
4 | from .douban import DouBan
5 | from .bangumi import Bangumi
6 |
--------------------------------------------------------------------------------
/app/media/doubanapi/__init__.py:
--------------------------------------------------------------------------------
1 | from .apiv2 import DoubanApi
2 | from .webapi import DoubanWeb
3 |
--------------------------------------------------------------------------------
/app/media/meta/__init__.py:
--------------------------------------------------------------------------------
1 | from .metainfo import MetaInfo
2 | from .metaanime import MetaAnime
3 | from ._base import MetaBase
4 | from .metavideo import MetaVideo
5 | from .metavideov2 import MetaVideoV2
6 | from .release_groups import ReleaseGroupsMatcher
7 | from .mediaItem import MediaMainItem, MediaEpisodeItem, MediaVideoItem,\
8 | MediaAudioItem, MediaLocalizationItem, MediaOtherItem, MediaItem
9 |
--------------------------------------------------------------------------------
/app/media/meta/customization.py:
--------------------------------------------------------------------------------
1 | import regex as re
2 | from app.utils.commons import singleton
3 |
4 |
5 | @singleton
6 | class CustomizationMatcher(object):
7 | """
8 | 识别自定义占位符
9 | """
10 | customization = None
11 | custom_separator = None
12 |
13 | def __init__(self):
14 | self.customization = None
15 | self.custom_separator = None
16 |
17 | def match(self, title=None):
18 | """
19 | :param title: 资源标题或文件名
20 | :return: 匹配结果
21 | """
22 | if not title:
23 | return ""
24 | if not self.customization:
25 | return ""
26 | customization_re = re.compile(r"%s" % self.customization)
27 | # 处理重复多次的情况,保留先后顺序(按添加自定义占位符的顺序)
28 | unique_customization = {}
29 | for item in re.findall(customization_re, title):
30 | if not isinstance(item, tuple):
31 | item = (item,)
32 | for i in range(len(item)):
33 | if item[i] and unique_customization.get(item[i]) is None:
34 | unique_customization[item[i]] = i
35 | unique_customization = list(dict(sorted(unique_customization.items(), key=lambda x: x[1])).keys())
36 | separator = self.custom_separator or "@"
37 | return separator.join(unique_customization)
38 |
39 | def update_custom(self, customization=None, separator=None):
40 | """
41 | 更新自定义占位符
42 | """
43 | self.customization = customization
44 | self.custom_separator = separator
45 |
--------------------------------------------------------------------------------
/app/media/tmdbv3api/__init__.py:
--------------------------------------------------------------------------------
1 | from .tmdb import TMDb
2 | from .exceptions import TMDbException
3 | from .objs.movie import Movie
4 | from .objs.search import Search
5 | from .objs.tv import TV
6 | from .objs.person import Person
7 | from .objs.find import Find
8 | from .objs.discover import Discover
9 | from .objs.trending import Trending
10 | from .objs.episode import Episode
11 | from .objs.genre import Genre
12 |
--------------------------------------------------------------------------------
/app/media/tmdbv3api/exceptions.py:
--------------------------------------------------------------------------------
1 | class TMDbException(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/app/media/tmdbv3api/objs/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/app/media/tmdbv3api/objs/__init__.py
--------------------------------------------------------------------------------
/app/media/tmdbv3api/objs/discover.py:
--------------------------------------------------------------------------------
1 | from app.media.tmdbv3api.tmdb import TMDb
2 |
3 | try:
4 | from urllib import urlencode
5 | except ImportError:
6 | from urllib.parse import urlencode
7 |
8 |
9 | class Discover(TMDb):
10 | _urls = {
11 | "movies": "/discover/movie",
12 | "tvs": "/discover/tv"
13 | }
14 |
15 | def discover_movies(self, params, page=1):
16 | """
17 | Discover movies by different types of data like average rating, number of votes, genres and certifications.
18 | :param params: dict
19 | :param page: int
20 | :return:
21 | """
22 | if not params:
23 | params = {}
24 | if page:
25 | params.update({"page": page})
26 | return self._get_obj(
27 | self._call(
28 | self._urls["movies"],
29 | urlencode(params)
30 | ),
31 | "results"
32 | )
33 |
34 | def discover_movies_pages(self, params):
35 | """
36 | Discover movies by different types of data like average rating, number of votes, genres and certifications.
37 | :param params: dict
38 | :return: total_pages
39 | """
40 | if not params:
41 | params = {}
42 | result = self._call(
43 | self._urls["movies"],
44 | urlencode(params)
45 | )
46 | return result.get("total_pages") or 0
47 |
48 | def discover_tv_shows(self, params, page=1):
49 | """
50 | Discover TV shows by different types of data like average rating, number of votes, genres,
51 | the network they aired on and air dates.
52 | :param params: dict
53 | :param page: int
54 | :return:
55 | """
56 | if not params:
57 | params = {}
58 | if page:
59 | params.update({"page": page})
60 | return self._get_obj(
61 | self._call(
62 | self._urls["tvs"],
63 | urlencode(params)
64 | ),
65 | "results"
66 | )
67 |
--------------------------------------------------------------------------------
/app/media/tmdbv3api/objs/episode.py:
--------------------------------------------------------------------------------
1 | from app.media.tmdbv3api.as_obj import AsObj
2 | from app.media.tmdbv3api.tmdb import TMDb
3 | from app.utils import StringUtils
4 |
5 | class Episode(TMDb):
6 | _urls = {
7 | "images": "/tv/%s/season/%s/episode/%s/images",
8 | "details": "/tv/%s/season/%s/episode/%s"
9 | }
10 |
11 | def images(self, tv_id, season_num, episode_num, include_image_language=None):
12 | """
13 | Get the images that belong to a TV episode.
14 | :param tv_id: int
15 | :param season_num: int
16 | :param episode_num: int
17 | :param include_image_language: str
18 | :return:
19 | """
20 | try:
21 | images = AsObj(
22 | **self._call(
23 | self._urls["details"] % (tv_id, season_num, episode_num),
24 | "language=%s" % include_image_language if include_image_language else "" + "&append_to_response=images"
25 | )
26 | )
27 | if not images:
28 | return None
29 | still_path = images.get("still_path")
30 | if isinstance(still_path, str):
31 | return [{"file_path": still_path}]
32 | elif isinstance(still_path, list):
33 | return [
34 | {"file_path": str(file_path)}
35 | for file_path in images
36 | if StringUtils.is_string_and_not_empty(file_path)
37 | ]
38 | else:
39 | return None
40 | except Exception as e:
41 | return None
42 |
--------------------------------------------------------------------------------
/app/media/tmdbv3api/objs/find.py:
--------------------------------------------------------------------------------
1 | from app.media.tmdbv3api.tmdb import TMDb
2 |
3 |
4 | class Find(TMDb):
5 | _urls = {
6 | "find": "/find/%s"
7 | }
8 |
9 | def find_by_imdbid(self, imdbid):
10 | return self._call(
11 | self._urls["find"] % imdbid,
12 | "external_source=imdb_id")
13 |
--------------------------------------------------------------------------------
/app/media/tmdbv3api/objs/genre.py:
--------------------------------------------------------------------------------
1 | from app.media.tmdbv3api.tmdb import TMDb
2 |
3 |
4 | class Genre(TMDb):
5 | _urls = {
6 | "movie_list": "/genre/movie/list",
7 | "tv_list": "/genre/tv/list"
8 | }
9 |
10 | def movie_list(self):
11 | """
12 | Get the list of official genres for movies.
13 | :return:
14 | """
15 | return self._get_obj(self._call(self._urls["movie_list"], ""), "genres")
16 |
17 | def tv_list(self):
18 | """
19 | Get the list of official genres for TV shows.
20 | :return:
21 | """
22 | return self._get_obj(self._call(self._urls["tv_list"], ""), "genres")
23 |
--------------------------------------------------------------------------------
/app/mediaserver/__init__.py:
--------------------------------------------------------------------------------
1 | from .media_server import MediaServer
2 |
--------------------------------------------------------------------------------
/app/mediaserver/client/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/app/mediaserver/client/__init__.py
--------------------------------------------------------------------------------
/app/message/__init__.py:
--------------------------------------------------------------------------------
1 | from .message import Message
2 | from .message_center import MessageCenter
3 |
--------------------------------------------------------------------------------
/app/message/client/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/app/message/client/__init__.py
--------------------------------------------------------------------------------
/app/message/client/_base.py:
--------------------------------------------------------------------------------
1 | from abc import ABCMeta, abstractmethod
2 |
3 |
4 | class _IMessageClient(metaclass=ABCMeta):
5 |
6 | @abstractmethod
7 | def match(self, ctype):
8 | """
9 | 匹配实例
10 | """
11 | pass
12 |
13 | @abstractmethod
14 | def send_msg(self, title, text, image, url, user_id):
15 | """
16 | 消息发送入口,支持文本、图片、链接跳转、指定发送对象
17 | :param title: 消息标题
18 | :param text: 消息内容
19 | :param image: 图片地址
20 | :param url: 点击消息跳转URL
21 | :param user_id: 消息发送对象的ID,为空则发给所有人
22 | :return: 发送状态,错误信息
23 | """
24 | pass
25 |
26 | @abstractmethod
27 | def send_list_msg(self, medias: list, user_id="", title="", url=""):
28 | """
29 | 发送列表类消息
30 | :param title: 消息标题
31 | :param medias: 媒体列表
32 | :param user_id: 消息发送对象的ID,为空则发给所有人
33 | :param url: 跳转链接地址
34 | """
35 | pass
36 |
--------------------------------------------------------------------------------
/app/message/client/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/pushdeer.py:
--------------------------------------------------------------------------------
1 | from pypushdeer import PushDeer
2 |
3 | from app.message.client._base import _IMessageClient
4 | from app.utils import StringUtils, ExceptionUtils
5 |
6 |
7 | class PushDeerClient(_IMessageClient):
8 | schema = "pushdeer"
9 |
10 | _server = None
11 | _apikey = None
12 | _client_config = {}
13 |
14 | def __init__(self, config):
15 | self._client_config = config
16 | self.init_config()
17 |
18 | def init_config(self):
19 | if self._client_config:
20 | self._server = StringUtils.get_base_url(self._client_config.get('server'))
21 | self._apikey = self._client_config.get('apikey')
22 |
23 | @classmethod
24 | def match(cls, ctype):
25 | return True if ctype == cls.schema else False
26 |
27 | def send_msg(self, title, text="", image="", url="", user_id=""):
28 | """
29 | 发送PushDeer消息
30 | :param title: 消息标题
31 | :param text: 消息内容
32 | :param image: 未使用
33 | :param url: 未使用
34 | :param user_id: 未使用
35 | :return: 发送状态、错误信息
36 | """
37 | if not title and not text:
38 | return False, "标题和内容不能同时为空"
39 | try:
40 | if not self._server or not self._apikey:
41 | return False, "参数未配置"
42 | pushdeer = PushDeer(server=self._server, pushkey=self._apikey)
43 | res = pushdeer.send_markdown(title, desp=text)
44 | if res:
45 | return True, "成功"
46 | else:
47 | return False, "失败"
48 | except Exception as msg_e:
49 | ExceptionUtils.exception_traceback(msg_e)
50 | return False, str(msg_e)
51 |
52 | def send_list_msg(self, **kwargs):
53 | pass
54 |
--------------------------------------------------------------------------------
/app/message/client/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/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, title, content=None):
17 | """
18 | 新增系统消息
19 | :param title: 标题
20 | :param content: 内容
21 | """
22 | title = title.replace("\n", "
").strip() if title else ""
23 | content = content.replace("\n", "
").strip() if content else ""
24 | self.__append_message_queue(title, content)
25 |
26 | def __append_message_queue(self, title, content):
27 | """
28 | 将消息增加到队列
29 | """
30 | self._message_queue.appendleft({"title": title,
31 | "content": content,
32 | "time": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))})
33 |
34 | def get_system_messages(self, num=20, lst_time=None):
35 | """
36 | 查询系统消息
37 | :param num:条数
38 | :param lst_time: 最后时间
39 | """
40 | if not lst_time:
41 | return list(self._message_queue)[-num:]
42 | else:
43 | ret_messages = []
44 | for message in list(self._message_queue):
45 | if (datetime.datetime.strptime(message.get("time"), '%Y-%m-%d %H:%M:%S') - datetime.datetime.strptime(
46 | lst_time, '%Y-%m-%d %H:%M:%S')).seconds > 0:
47 | ret_messages.append(message)
48 | else:
49 | break
50 | return ret_messages
51 |
--------------------------------------------------------------------------------
/app/plugins/__init__.py:
--------------------------------------------------------------------------------
1 | from .event_manager import EventManager, EventHandler, Event
2 | from .plugin_manager import PluginManager
3 |
--------------------------------------------------------------------------------
/app/plugins/modules/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/app/plugins/modules/__init__.py
--------------------------------------------------------------------------------
/app/plugins/modules/_autosignin/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/app/plugins/modules/_autosignin/__init__.py
--------------------------------------------------------------------------------
/app/plugins/modules/_autosignin/_base.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import re
3 | from abc import ABCMeta, abstractmethod
4 |
5 | import log
6 | from app.utils import StringUtils
7 |
8 |
9 | class _ISiteSigninHandler(metaclass=ABCMeta):
10 | """
11 | 实现站点签到的基类,所有站点签到类都需要继承此类,并实现match和signin方法
12 | 实现类放置到sitesignin目录下将会自动加载
13 | """
14 | # 匹配的站点Url,每一个实现类都需要设置为自己的站点Url
15 | site_url = ""
16 |
17 | @abstractmethod
18 | def match(self, url):
19 | """
20 | 根据站点Url判断是否匹配当前站点签到类,大部分情况使用默认实现即可
21 | :param url: 站点Url
22 | :return: 是否匹配,如匹配则会调用该类的signin方法
23 | """
24 | return True if StringUtils.url_equal(url, self.site_url) else False
25 |
26 | @abstractmethod
27 | def signin(self, site_info: dict):
28 | """
29 | 执行签到操作
30 | :param site_info: 站点信息,含有站点Url、站点Cookie、UA等信息
31 | :return: True|False,签到结果信息
32 | """
33 | pass
34 |
35 | @staticmethod
36 | def sign_in_result(html_res, regexs):
37 | """
38 | 判断是否签到成功
39 | """
40 | html_text = re.sub(r"#\d+", "", re.sub(r"\d+px", "", html_res))
41 | for regex in regexs:
42 | if re.search(str(regex), html_text):
43 | return True
44 | return False
45 |
46 | def info(self, msg):
47 | """
48 | 记录INFO日志
49 | :param msg: 日志信息
50 | """
51 | log.info(f"【Sites】{self.__class__.__name__} - {msg}")
52 |
53 | def warn(self, msg):
54 | """
55 | 记录WARN日志
56 | :param msg: 日志信息
57 | """
58 | log.warn(f"【Sites】{self.__class__.__name__} - {msg}")
59 |
60 | def error(self, msg):
61 | """
62 | 记录ERROR日志
63 | :param msg: 日志信息
64 | """
65 | log.error(f"【Sites】{self.__class__.__name__} - {msg}")
66 |
67 | def debug(self, msg):
68 | """
69 | 记录Debug日志
70 | :param msg: 日志信息
71 | """
72 | log.debug(f"【Sites】{self.__class__.__name__} - {msg}")
73 |
--------------------------------------------------------------------------------
/app/plugins/modules/_autosignin/carpt.py:
--------------------------------------------------------------------------------
1 | from app.plugins.modules._autosignin._base import _ISiteSigninHandler
2 | from app.utils import StringUtils, RequestUtils
3 | from config import Config
4 |
5 |
6 | class CarPT(_ISiteSigninHandler):
7 | """
8 | 车站签到
9 | """
10 |
11 | # 匹配的站点Url,每一个实现类都需要设置为自己的站点Url
12 | site_url = "carpt.net"
13 |
14 | # 签到成功
15 | _success_text = "签到成功"
16 | _repeat_text = "请不要重复签到哦"
17 |
18 | @classmethod
19 | def match(cls, url):
20 | """
21 | 根据站点Url判断是否匹配当前站点签到类,大部分情况使用默认实现即可
22 | :param url: 站点Url
23 | :return: 是否匹配,如匹配则会调用该类的signin方法
24 | """
25 | return True if StringUtils.url_equal(url, cls.site_url) else False
26 |
27 | def signin(self, site_info: dict):
28 | """
29 | 执行签到操作
30 | :param site_info: 站点信息,含有站点Url、站点Cookie、UA等信息
31 | :return: 签到结果信息
32 | """
33 | site = site_info.get("name")
34 | site_cookie = site_info.get("cookie")
35 | ua = site_info.get("ua")
36 | proxy = Config().get_proxies() if site_info.get("proxy") else None
37 |
38 | # 获取页面html
39 | html_res = RequestUtils(cookies=site_cookie,
40 | headers=ua,
41 | proxies=proxy
42 | ).get_res(url="https://carpt.net/attendance.php")
43 | if not html_res or html_res.status_code != 200:
44 | self.error(f"签到失败,请检查站点连通性")
45 | return False, f'【{site}】签到失败,请检查站点连通性'
46 |
47 | if "login.php" in html_res.text:
48 | self.error(f"签到失败,cookie失效")
49 | return False, f'【{site}】签到失败,cookie失效'
50 |
51 | # 判断是否已签到
52 | # '已连续签到278天,此次签到您获得了100魔力值奖励!'
53 | if self._success_text in html_res.text:
54 | self.info(f"签到成功")
55 | return True, f'【{site}】签到成功'
56 | if self._repeat_text in html_res.text:
57 | self.info(f"今日已签到")
58 | return True, f'【{site}】今日已签到'
59 | self.error(f"签到失败,签到接口返回 {html_res.text}")
60 | return False, f'【{site}】签到失败'
61 |
--------------------------------------------------------------------------------
/app/plugins/modules/_autosignin/haidan.py:
--------------------------------------------------------------------------------
1 | from app.plugins.modules._autosignin._base import _ISiteSigninHandler
2 | from app.utils import StringUtils, RequestUtils
3 | from config import Config
4 |
5 |
6 | class HaiDan(_ISiteSigninHandler):
7 | """
8 | 海胆签到
9 | """
10 | # 匹配的站点Url,每一个实现类都需要设置为自己的站点Url
11 | site_url = "haidan.video"
12 |
13 | # 签到成功
14 | _succeed_regex = ['(?<=value=")已经打卡(?=")']
15 |
16 | @classmethod
17 | def match(cls, url):
18 | """
19 | 根据站点Url判断是否匹配当前站点签到类,大部分情况使用默认实现即可
20 | :param url: 站点Url
21 | :return: 是否匹配,如匹配则会调用该类的signin方法
22 | """
23 | return True if StringUtils.url_equal(url, cls.site_url) else False
24 |
25 | def signin(self, site_info: dict):
26 | """
27 | 执行签到操作
28 | :param site_info: 站点信息,含有站点Url、站点Cookie、UA等信息
29 | :return: 签到结果信息
30 | """
31 | site = site_info.get("name")
32 | site_cookie = site_info.get("cookie")
33 | ua = site_info.get("ua")
34 | proxy = Config().get_proxies() if site_info.get("proxy") else None
35 |
36 | # 签到
37 | sign_res = RequestUtils(cookies=site_cookie,
38 | headers=ua,
39 | proxies=proxy
40 | ).get_res(url="https://www.haidan.video/signin.php")
41 | if not sign_res or sign_res.status_code != 200:
42 | self.error(f"签到失败,请检查站点连通性")
43 | return False, f'【{site}】签到失败,请检查站点连通性'
44 |
45 | if "login.php" in sign_res.text:
46 | self.error(f"签到失败,cookie失效")
47 | return False, f'【{site}】签到失败,cookie失效'
48 |
49 | sign_status = self.sign_in_result(html_res=sign_res.text,
50 | regexs=self._succeed_regex)
51 | if sign_status:
52 | self.info(f"签到成功")
53 | return True, f'【{site}】签到成功'
54 |
55 | self.error(f"签到失败,签到接口返回 {sign_res.text}")
56 | return False, f'【{site}】签到失败'
57 |
--------------------------------------------------------------------------------
/app/plugins/modules/_autosignin/hdarea.py:
--------------------------------------------------------------------------------
1 | from app.plugins.modules._autosignin._base import _ISiteSigninHandler
2 | from app.utils import StringUtils, RequestUtils
3 | from config import Config
4 |
5 |
6 | class HDArea(_ISiteSigninHandler):
7 | """
8 | 好大签到
9 | """
10 |
11 | # 匹配的站点Url,每一个实现类都需要设置为自己的站点Url
12 | site_url = "hdarea.club"
13 |
14 | # 签到成功
15 | _success_text = "此次签到您获得"
16 | _repeat_text = "请不要重复签到哦"
17 |
18 | @classmethod
19 | def match(cls, url):
20 | """
21 | 根据站点Url判断是否匹配当前站点签到类,大部分情况使用默认实现即可
22 | :param url: 站点Url
23 | :return: 是否匹配,如匹配则会调用该类的signin方法
24 | """
25 | return True if StringUtils.url_equal(url, cls.site_url) else False
26 |
27 | def signin(self, site_info: dict):
28 | """
29 | 执行签到操作
30 | :param site_info: 站点信息,含有站点Url、站点Cookie、UA等信息
31 | :return: 签到结果信息
32 | """
33 | site = site_info.get("name")
34 | site_cookie = site_info.get("cookie")
35 | ua = site_info.get("ua")
36 | proxy = Config().get_proxies() if site_info.get("proxy") else None
37 |
38 | # 获取页面html
39 | data = {
40 | 'action': 'sign_in'
41 | }
42 | html_res = RequestUtils(cookies=site_cookie,
43 | headers=ua,
44 | proxies=proxy
45 | ).post_res(url="https://hdarea.club/sign_in.php", data=data)
46 | if not html_res or html_res.status_code != 200:
47 | self.error(f"签到失败,请检查站点连通性")
48 | return False, f'【{site}】签到失败,请检查站点连通性'
49 |
50 | if "login.php" in html_res.text:
51 | self.error(f"签到失败,cookie失效")
52 | return False, f'【{site}】签到失败,cookie失效'
53 |
54 | # 判断是否已签到
55 | # '已连续签到278天,此次签到您获得了100魔力值奖励!'
56 | if self._success_text in html_res.text:
57 | self.info(f"签到成功")
58 | return True, f'【{site}】签到成功'
59 | if self._repeat_text in html_res.text:
60 | self.info(f"今日已签到")
61 | return True, f'【{site}】今日已签到'
62 | self.error(f"签到失败,签到接口返回 {html_res.text}")
63 | return False, f'【{site}】签到失败'
64 |
--------------------------------------------------------------------------------
/app/plugins/modules/_autosignin/hdcity.py:
--------------------------------------------------------------------------------
1 | from app.plugins.modules._autosignin._base import _ISiteSigninHandler
2 | from app.utils import StringUtils, RequestUtils
3 | from config import Config
4 |
5 |
6 | class HDCity(_ISiteSigninHandler):
7 | """
8 | 城市签到
9 | """
10 | # 匹配的站点Url,每一个实现类都需要设置为自己的站点Url
11 | site_url = "hdcity.city"
12 |
13 | # 签到成功
14 | _success_text = '本次签到获得魅力'
15 | # 重复签到
16 | _repeat_text = '已签到'
17 |
18 | @classmethod
19 | def match(cls, url):
20 | """
21 | 根据站点Url判断是否匹配当前站点签到类,大部分情况使用默认实现即可
22 | :param url: 站点Url
23 | :return: 是否匹配,如匹配则会调用该类的signin方法
24 | """
25 | return True if StringUtils.url_equal(url, cls.site_url) else False
26 |
27 | def signin(self, site_info: dict):
28 | """
29 | 执行签到操作
30 | :param site_info: 站点信息,含有站点Url、站点Cookie、UA等信息
31 | :return: 签到结果信息
32 | """
33 | site = site_info.get("name")
34 | site_cookie = site_info.get("cookie")
35 | ua = site_info.get("ua")
36 | proxy = Config().get_proxies() if site_info.get("proxy") else None
37 |
38 | # 获取页面html
39 | html_res = RequestUtils(cookies=site_cookie,
40 | headers=ua,
41 | proxies=proxy
42 | ).get_res(url="https://hdcity.city/sign")
43 | if not html_res or html_res.status_code != 200:
44 | self.error(f"签到失败,请检查站点连通性")
45 | return False, f'【{site}】签到失败,请检查站点连通性'
46 |
47 | if "login" in html_res.text:
48 | self.error(f"签到失败,cookie失效")
49 | return False, f'【{site}】签到失败,cookie失效'
50 |
51 | # 判断是否已签到
52 | # '已连续签到278天,此次签到您获得了100魔力值奖励!'
53 | if self._success_text in html_res.text:
54 | self.info(f"签到成功")
55 | return True, f'【{site}】签到成功'
56 | if self._repeat_text in html_res.text:
57 | self.info(f"今日已签到")
58 | return True, f'【{site}】今日已签到'
59 | self.error(f"签到失败,签到接口返回 {html_res.text}")
60 | return False, f'【{site}】签到失败'
61 |
--------------------------------------------------------------------------------
/app/plugins/modules/_autosignin/hdfans.py:
--------------------------------------------------------------------------------
1 | from app.plugins.modules._autosignin._base import _ISiteSigninHandler
2 | from app.utils import StringUtils, RequestUtils
3 | from config import Config
4 |
5 |
6 | class HDFans(_ISiteSigninHandler):
7 | """
8 | hdfans签到
9 | """
10 |
11 | # 匹配的站点Url,每一个实现类都需要设置为自己的站点Url
12 | site_url = "hdfans.org"
13 |
14 | # 签到成功
15 | _success_text = "签到成功"
16 | _repeat_text = "请不要重复签到哦"
17 |
18 | @classmethod
19 | def match(cls, url):
20 | """
21 | 根据站点Url判断是否匹配当前站点签到类,大部分情况使用默认实现即可
22 | :param url: 站点Url
23 | :return: 是否匹配,如匹配则会调用该类的signin方法
24 | """
25 | return True if StringUtils.url_equal(url, cls.site_url) else False
26 |
27 | def signin(self, site_info: dict):
28 | """
29 | 执行签到操作
30 | :param site_info: 站点信息,含有站点Url、站点Cookie、UA等信息
31 | :return: 签到结果信息
32 | """
33 | site = site_info.get("name")
34 | site_cookie = site_info.get("cookie")
35 | ua = site_info.get("ua")
36 | proxy = Config().get_proxies() if site_info.get("proxy") else None
37 |
38 | # 获取页面html
39 | html_res = RequestUtils(cookies=site_cookie,
40 | headers=ua,
41 | proxies=proxy
42 | ).get_res(url="https://hdfans.org/attendance.php")
43 | if not html_res or html_res.status_code != 200:
44 | self.error(f"签到失败,请检查站点连通性")
45 | return False, f'【{site}】签到失败,请检查站点连通性'
46 |
47 | if "login.php" in html_res.text:
48 | self.error(f"签到失败,cookie失效")
49 | return False, f'【{site}】签到失败,cookie失效'
50 |
51 | # 判断是否已签到
52 | # '已连续签到278天,此次签到您获得了100魔力值奖励!'
53 | if self._success_text in html_res.text:
54 | self.info(f"签到成功")
55 | return True, f'【{site}】签到成功'
56 | if self._repeat_text in html_res.text:
57 | self.info(f"今日已签到")
58 | return True, f'【{site}】今日已签到'
59 | self.error(f"签到失败,签到接口返回 {html_res.text}")
60 | return False, f'【{site}】签到失败'
61 |
--------------------------------------------------------------------------------
/app/plugins/modules/_autosignin/hdtime.py:
--------------------------------------------------------------------------------
1 | from app.plugins.modules._autosignin._base import _ISiteSigninHandler
2 | from app.utils import StringUtils, RequestUtils
3 | from config import Config
4 |
5 |
6 | class HDTime(_ISiteSigninHandler):
7 | """
8 | 时光签到
9 | """
10 |
11 | # 匹配的站点Url,每一个实现类都需要设置为自己的站点Url
12 | site_url = "hdtime.org"
13 |
14 | # 签到成功
15 | _success_text = "签到成功"
16 | _repeat_text = "请不要重复签到哦"
17 |
18 | @classmethod
19 | def match(cls, url):
20 | """
21 | 根据站点Url判断是否匹配当前站点签到类,大部分情况使用默认实现即可
22 | :param url: 站点Url
23 | :return: 是否匹配,如匹配则会调用该类的signin方法
24 | """
25 | return True if StringUtils.url_equal(url, cls.site_url) else False
26 |
27 | def signin(self, site_info: dict):
28 | """
29 | 执行签到操作
30 | :param site_info: 站点信息,含有站点Url、站点Cookie、UA等信息
31 | :return: 签到结果信息
32 | """
33 | site = site_info.get("name")
34 | site_cookie = site_info.get("cookie")
35 | ua = site_info.get("ua")
36 | proxy = Config().get_proxies() if site_info.get("proxy") else None
37 |
38 | # 获取页面html
39 | html_res = RequestUtils(cookies=site_cookie,
40 | headers=ua,
41 | proxies=proxy
42 | ).get_res(url="https://hdtime.org/attendance.php")
43 | if not html_res or html_res.status_code != 200:
44 | self.error(f"签到失败,请检查站点连通性")
45 | return False, f'【{site}】签到失败,请检查站点连通性'
46 |
47 | if "login.php" in html_res.text:
48 | self.error(f"签到失败,cookie失效")
49 | return False, f'【{site}】签到失败,cookie失效'
50 |
51 | # 判断是否已签到
52 | # '已连续签到278天,此次签到您获得了100魔力值奖励!'
53 | if self._success_text in html_res.text:
54 | self.info(f"签到成功")
55 | return True, f'【{site}】签到成功'
56 | if self._repeat_text in html_res.text:
57 | self.info(f"今日已签到")
58 | return True, f'【{site}】今日已签到'
59 | self.error(f"签到失败,签到接口返回 {html_res.text}")
60 | return False, f'【{site}】签到失败'
61 |
--------------------------------------------------------------------------------
/app/plugins/modules/_autosignin/hhanclub.py:
--------------------------------------------------------------------------------
1 | from app.plugins.modules._autosignin._base import _ISiteSigninHandler
2 | from app.utils import StringUtils, RequestUtils
3 | from config import Config
4 |
5 |
6 | class Hhanclub(_ISiteSigninHandler):
7 | """
8 | 海胆签到
9 | """
10 | # 匹配的站点Url,每一个实现类都需要设置为自己的站点Url
11 | site_url = "hhanclub.top"
12 |
13 | # 签到成功
14 | _succeed_regex = ["今日签到排名:(\\d+) / \\d+"]
15 |
16 | @classmethod
17 | def match(cls, url):
18 | """
19 | 根据站点Url判断是否匹配当前站点签到类,大部分情况使用默认实现即可
20 | :param url: 站点Url
21 | :return: 是否匹配,如匹配则会调用该类的signin方法
22 | """
23 | return True if StringUtils.url_equal(url, cls.site_url) else False
24 |
25 | def signin(self, site_info: dict):
26 | """
27 | 执行签到操作
28 | :param site_info: 站点信息,含有站点Url、站点Cookie、UA等信息
29 | :return: 签到结果信息
30 | """
31 | site = site_info.get("name")
32 | site_cookie = site_info.get("cookie")
33 | ua = site_info.get("ua")
34 | proxy = Config().get_proxies() if site_info.get("proxy") else None
35 |
36 | # 签到
37 | sign_res = RequestUtils(cookies=site_cookie,
38 | headers=ua,
39 | proxies=proxy
40 | ).get_res(url="https://hhanclub.top/attendance.php")
41 | if not sign_res or sign_res.status_code != 200:
42 | self.error(f"签到失败,请检查站点连通性")
43 | return False, f'【{site}】签到失败,请检查站点连通性'
44 |
45 | if "rule-tips" not in sign_res.text:
46 | self.error(f"签到失败,cookie失效")
47 | return False, f'【{site}】签到失败,cookie失效'
48 |
49 | sign_status = self.sign_in_result(html_res=sign_res.text,
50 | regexs=self._succeed_regex)
51 | if sign_status:
52 | self.info(f"签到成功")
53 | return True, f'【{site}】签到成功'
54 |
55 | self.error(f"签到失败,签到接口返回 {sign_res.text}")
56 | return False, f'【{site}】签到失败'
57 |
--------------------------------------------------------------------------------
/app/plugins/modules/_autosignin/pterclub.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from app.plugins.modules._autosignin._base import _ISiteSigninHandler
4 | from app.utils import StringUtils, RequestUtils
5 | from config import Config
6 |
7 |
8 | class PTerClub(_ISiteSigninHandler):
9 | """
10 | 猫签到
11 | """
12 | # 匹配的站点Url,每一个实现类都需要设置为自己的站点Url
13 | site_url = "pterclub.com"
14 |
15 |
16 | @classmethod
17 | def match(cls, url):
18 | """
19 | 根据站点Url判断是否匹配当前站点签到类,大部分情况使用默认实现即可
20 | :param url: 站点Url
21 | :return: 是否匹配,如匹配则会调用该类的signin方法
22 | """
23 | return True if StringUtils.url_equal(url, cls.site_url) else False
24 |
25 | def signin(self, site_info: dict):
26 | """
27 | 执行签到操作
28 | :param site_info: 站点信息,含有站点Url、站点Cookie、UA等信息
29 | :return: 签到结果信息
30 | """
31 | site = site_info.get("name")
32 | site_cookie = site_info.get("cookie")
33 | ua = site_info.get("ua")
34 | proxy = Config().get_proxies() if site_info.get("proxy") else None
35 |
36 | # 签到
37 | sign_res = RequestUtils(cookies=site_cookie,
38 | headers=ua,
39 | proxies=proxy
40 | ).get_res(url="https://pterclub.com/attendance-ajax.php")
41 | if not sign_res or sign_res.status_code != 200:
42 | self.error(f"签到失败,签到接口请求失败")
43 | return False, f'【{site}】签到失败,请检查cookie是否失效'
44 |
45 | sign_dict = json.loads(sign_res.text)
46 | if sign_dict['status'] == '1':
47 | # {"status":"1","data":" (签到已成功300)","message":"
这是您的第237次签到, 48 | # 已连续签到237天。
本次签到获得300克猫粮。
"} 49 | self.info(f"签到成功") 50 | return True, f'【{site}】签到成功' 51 | else: 52 | # {"status":"0","data":"抱歉","message":"您今天已经签到过了,请勿重复刷新。"} 53 | self.info(f"今日已签到") 54 | return True, f'【{site}】今日已签到' 55 | -------------------------------------------------------------------------------- /app/plugins/modules/_autosignin/pttime.py: -------------------------------------------------------------------------------- 1 | from app.plugins.modules._autosignin._base import _ISiteSigninHandler 2 | from app.utils import StringUtils, RequestUtils 3 | from config import Config 4 | 5 | 6 | class PTTime(_ISiteSigninHandler): 7 | """ 8 | 车站签到 9 | """ 10 | 11 | # 匹配的站点Url,每一个实现类都需要设置为自己的站点Url 12 | site_url = "pttime.org" 13 | 14 | # 签到成功 15 | _success_text = "签到成功" 16 | _repeat_text = "今天已签到,请勿重复刷新" 17 | 18 | @classmethod 19 | def match(cls, url): 20 | """ 21 | 根据站点Url判断是否匹配当前站点签到类,大部分情况使用默认实现即可 22 | :param url: 站点Url 23 | :return: 是否匹配,如匹配则会调用该类的signin方法 24 | """ 25 | return True if StringUtils.url_equal(url, cls.site_url) else False 26 | 27 | def signin(self, site_info: dict): 28 | """ 29 | 执行签到操作 30 | :param site_info: 站点信息,含有站点Url、站点Cookie、UA等信息 31 | :return: 签到结果信息 32 | """ 33 | site = site_info.get("name") 34 | site_cookie = site_info.get("cookie") 35 | ua = site_info.get("ua") 36 | proxy = Config().get_proxies() if site_info.get("proxy") else None 37 | 38 | # 获取页面html 39 | html_res = RequestUtils(cookies=site_cookie, 40 | headers=ua, 41 | proxies=proxy 42 | ).get_res(url="https://www.pttime.org/attendance.php") 43 | if not html_res or html_res.status_code != 200: 44 | self.error(f"签到失败,请检查站点连通性") 45 | return False, f'【{site}】签到失败,请检查站点连通性' 46 | 47 | if "login.php" in html_res.text: 48 | self.error(f"签到失败,cookie失效") 49 | return False, f'【{site}】签到失败,cookie失效' 50 | 51 | # 判断是否已签到 52 | # '已连续签到278天,此次签到您获得了100魔力值奖励!' 53 | if self._success_text in html_res.text: 54 | self.info(f"签到成功") 55 | return True, f'【{site}】签到成功' 56 | if self._repeat_text in html_res.text: 57 | self.info(f"今日已签到") 58 | return True, f'【{site}】今日已签到' 59 | self.error(f"签到失败,签到接口返回 {html_res.text}") 60 | return False, f'【{site}】签到失败' 61 | -------------------------------------------------------------------------------- /app/plugins/modules/iyuu/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/app/plugins/modules/iyuu/__init__.py -------------------------------------------------------------------------------- /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_subtitle import SiteSubtitle 5 | from .siteconf import SiteConf 6 | from .site_limiter import SiteRateLimiter 7 | -------------------------------------------------------------------------------- /app/sites/site_limiter.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class SiteRateLimiter: 5 | def __init__(self, limit_interval: int, limit_count: int, limit_seconds: int): 6 | """ 7 | 限制访问频率 8 | :param limit_interval: 单位时间(秒) 9 | :param limit_count: 单位时间内访问次数 10 | :param limit_seconds: 访问间隔(秒) 11 | """ 12 | self.limit_count = limit_count 13 | self.limit_interval = limit_interval 14 | self.limit_seconds = limit_seconds 15 | self.last_visit_time = 0 16 | self.count = 0 17 | 18 | def check_rate_limit(self) -> (bool, str): 19 | """ 20 | 检查是否超出访问频率控制 21 | :return: 超出返回True,否则返回False,超出时返回错误信息 22 | """ 23 | current_time = time.time() 24 | # 防问间隔时间 25 | if self.limit_seconds: 26 | if current_time - self.last_visit_time < self.limit_seconds: 27 | return True, f"触发流控规则,访问间隔不得小于 {self.limit_seconds} 秒," \ 28 | f"上次访问时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.last_visit_time))}" 29 | # 单位时间内访问次数 30 | if self.limit_interval and self.limit_count: 31 | if current_time - self.last_visit_time > self.limit_interval: 32 | # 计数清零 33 | self.count = 0 34 | if self.count >= self.limit_count: 35 | return True, f"触发流控规则,{self.limit_interval} 秒内访问次数不得超过 {self.limit_count} 次," \ 36 | f"上次访问时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.last_visit_time))}" 37 | # 访问计数 38 | self.count += 1 39 | # 更新最后访问时间 40 | self.last_visit_time = current_time 41 | # 未触发流控 42 | return False, "" 43 | 44 | 45 | if __name__ == "__main__": 46 | # 限制 1 分钟内最多访问 10 次,单次访问间隔不得小于 10 秒 47 | site_rate_limit = SiteRateLimiter(10, 60, 10) 48 | 49 | # 模拟访问 50 | for i in range(12): 51 | if site_rate_limit.check_rate_limit(): 52 | print("访问频率超限") 53 | else: 54 | print("访问成功") 55 | time.sleep(3) 56 | -------------------------------------------------------------------------------- /app/sites/siteuserinfo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/app/sites/siteuserinfo/__init__.py -------------------------------------------------------------------------------- /app/sites/siteuserinfo/nexus_project.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | from app.sites.siteuserinfo._base import SITE_BASE_ORDER 5 | from app.sites.siteuserinfo.nexus_php import NexusPhpSiteUserInfo 6 | from app.utils.types import SiteSchema 7 | 8 | 9 | class NexusProjectSiteUserInfo(NexusPhpSiteUserInfo): 10 | schema = SiteSchema.NexusProject 11 | order = SITE_BASE_ORDER + 25 12 | 13 | @classmethod 14 | def match(cls, html_text): 15 | return 'Nexus Project' in html_text 16 | 17 | def _parse_site_page(self, html_text): 18 | html_text = self._prepare_html_text(html_text) 19 | 20 | user_detail = re.search(r"userdetails.php\?id=(\d+)", html_text) 21 | if user_detail and user_detail.group().strip(): 22 | self._user_detail_page = user_detail.group().strip().lstrip('/') 23 | self.userid = user_detail.group(1) 24 | 25 | self._torrent_seeding_page = f"viewusertorrents.php?id={self.userid}&show=seeding" 26 | -------------------------------------------------------------------------------- /app/sites/siteuserinfo/nexus_rabbit.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | 4 | from lxml import etree 5 | 6 | from app.sites.siteuserinfo._base import SITE_BASE_ORDER 7 | from app.sites.siteuserinfo.nexus_php import NexusPhpSiteUserInfo 8 | from app.utils.exception_utils import ExceptionUtils 9 | from app.utils.types import SiteSchema 10 | 11 | 12 | class NexusRabbitSiteUserInfo(NexusPhpSiteUserInfo): 13 | schema = SiteSchema.NexusRabbit 14 | order = SITE_BASE_ORDER + 5 15 | 16 | @classmethod 17 | def match(cls, html_text): 18 | html = etree.HTML(html_text) 19 | if not html: 20 | return False 21 | 22 | printable_text = html.xpath("string(.)") if html else "" 23 | return 'Style by Rabbit' in printable_text 24 | 25 | def _parse_site_page(self, html_text): 26 | super()._parse_site_page(html_text) 27 | self._torrent_seeding_page = f"getusertorrentlistajax.php?page=1&limit=5000000&type=seeding&uid={self.userid}" 28 | self._torrent_seeding_headers = {"Accept": "application/json, text/javascript, */*; q=0.01"} 29 | 30 | def _parse_user_torrent_seeding_info(self, html_text, multi_page=False): 31 | """ 32 | 做种相关信息 33 | :param html_text: 34 | :param multi_page: 是否多页数据 35 | :return: 下页地址 36 | """ 37 | 38 | try: 39 | torrents = json.loads(html_text).get('data') 40 | except Exception as e: 41 | ExceptionUtils.exception_traceback(e) 42 | return 43 | 44 | page_seeding_size = 0 45 | page_seeding_info = [] 46 | 47 | page_seeding = len(torrents) 48 | for torrent in torrents: 49 | seeders = int(torrent.get('seeders', 0)) 50 | size = int(torrent.get('size', 0)) 51 | page_seeding_size += int(torrent.get('size', 0)) 52 | 53 | page_seeding_info.append([seeders, size]) 54 | 55 | self.seeding += page_seeding 56 | self.seeding_size += page_seeding_size 57 | self.seeding_info.extend(page_seeding_info) 58 | -------------------------------------------------------------------------------- /app/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, CategoryLoadCache, 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 .image_utils import ImageUtils 17 | from .scheduler_utils import SchedulerUtils 18 | -------------------------------------------------------------------------------- /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 | CategoryLoadCache = Cache(maxsize=2, ttl=3, timer=time.time, default=None) 17 | 18 | OpenAISessionCache = Cache(maxsize=100, ttl=3600, timer=time.time, default=None) 19 | -------------------------------------------------------------------------------- /app/utils/dom_utils.py: -------------------------------------------------------------------------------- 1 | class DomUtils: 2 | 3 | @staticmethod 4 | def tag_value(tag_item, tag_name, attname="", default=None): 5 | """ 6 | 解析XML标签值 7 | """ 8 | tagNames = tag_item.getElementsByTagName(tag_name) 9 | if tagNames: 10 | if attname: 11 | attvalue = tagNames[0].getAttribute(attname) 12 | if attvalue: 13 | return attvalue 14 | else: 15 | firstChild = tagNames[0].firstChild 16 | if firstChild: 17 | return firstChild.data 18 | return default 19 | 20 | @staticmethod 21 | def add_node(doc, parent, name, value=None): 22 | """ 23 | 添加一个DOM节点 24 | """ 25 | node = doc.createElement(name) 26 | parent.appendChild(node) 27 | if value is not None: 28 | text = doc.createTextNode(str(value)) 29 | node.appendChild(text) 30 | return node 31 | -------------------------------------------------------------------------------- /app/utils/exception_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import traceback 3 | 4 | 5 | class ExceptionUtils: 6 | @classmethod 7 | def exception_traceback(cls, e): 8 | print(f"\nException: {str(e)}\nCallstack:\n{traceback.format_exc()}\n") 9 | -------------------------------------------------------------------------------- /app/utils/image_utils.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | from collections import Counter 3 | 4 | 5 | class ImageUtils: 6 | 7 | @staticmethod 8 | def calculate_theme_color(image_path): 9 | # 打开图片并转换为RGB模式 10 | img = Image.open(image_path).convert('RGB') 11 | # 缩小图片尺寸以加快计算速度 12 | img = img.resize((100, 100), resample=Image.BILINEAR) 13 | # 获取所有像素颜色值 14 | pixels = img.getdata() 15 | # 统计每种颜色在像素中出现的频率 16 | pixel_count = Counter(pixels) 17 | # 找到出现频率最高的颜色,作为主题色 18 | dominant_color = pixel_count.most_common(1)[0][0] 19 | # 将主题色转换为16进制表示 20 | theme_color = '#{:02x}{:02x}{:02x}'.format(*dominant_color) 21 | # 返回主题色 22 | return theme_color 23 | -------------------------------------------------------------------------------- /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/utils/json_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from enum import Enum 3 | 4 | 5 | class JsonUtils: 6 | 7 | @staticmethod 8 | def json_serializable(obj): 9 | """ 10 | 将普通对象转化为支持json序列化的对象 11 | @param obj: 待转化的对象 12 | @return: 支持json序列化的对象 13 | """ 14 | 15 | def _try(o): 16 | if isinstance(o, Enum): 17 | return o.value 18 | try: 19 | return o.__dict__ 20 | except Exception as err: 21 | print(str(err)) 22 | return str(o) 23 | 24 | return json.loads(json.dumps(obj, default=lambda o: _try(o))) 25 | -------------------------------------------------------------------------------- /app/utils/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/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/rsstitle_utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from app.utils.exception_utils import ExceptionUtils 4 | 5 | 6 | class RssTitleUtils: 7 | 8 | @staticmethod 9 | def keepfriends_title(title): 10 | """ 11 | 处理pt.keepfrds.com的RSS标题 12 | """ 13 | if not title: 14 | return "" 15 | try: 16 | title_search = re.search(r"\[(.*)]", title, re.IGNORECASE) 17 | if title_search: 18 | if title_search.span()[0] == 0: 19 | title_all = re.findall(r"\[(.*?)]", title, re.IGNORECASE) 20 | if title_all and len(title_all) > 1: 21 | torrent_name = title_all[-1] 22 | torrent_desc = title.replace(f"[{torrent_name}]", "").strip() 23 | title = "%s %s" % (torrent_name, torrent_desc) 24 | else: 25 | torrent_name = title_search.group(1) 26 | torrent_desc = title.replace(title_search.group(), "").strip() 27 | title = "%s %s" % (torrent_name, torrent_desc) 28 | except Exception as err: 29 | ExceptionUtils.exception_traceback(err) 30 | return title 31 | -------------------------------------------------------------------------------- /app/utils/tokens.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from config import SPLIT_CHARS 4 | 5 | 6 | class Tokens: 7 | _text = "" 8 | _index = 0 9 | _tokens = [] 10 | 11 | def __init__(self, text): 12 | self._text = text 13 | self._tokens = [] 14 | self.load_text(text) 15 | 16 | def load_text(self, text): 17 | splited_text = re.split(r'%s' % SPLIT_CHARS, text) 18 | for sub_text in splited_text: 19 | if sub_text: 20 | self._tokens.append(sub_text) 21 | 22 | def cur(self): 23 | if self._index >= len(self._tokens): 24 | return None 25 | else: 26 | token = self._tokens[self._index] 27 | return token 28 | 29 | def get_next(self): 30 | token = self.cur() 31 | if token: 32 | self._index = self._index + 1 33 | return token 34 | 35 | def peek(self): 36 | index = self._index + 1 37 | if index >= len(self._tokens): 38 | return None 39 | else: 40 | return self._tokens[index] 41 | -------------------------------------------------------------------------------- /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__), 'scripts').replace('\\', '/') 9 | alembic_cfg = AlembicConfig() 10 | alembic_cfg.set_main_option('script_location', script_location) 11 | alembic_cfg.set_main_option('sqlalchemy.url', f"sqlite:///{db_location}") 12 | alembic_revision(alembic_cfg, db_version, True) 13 | -------------------------------------------------------------------------------- /docker/compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | nas-tools: 4 | image: hsuyelin/nas-tools:latest 5 | ports: 6 | - 3000:3000 # 默认的webui控制端口 7 | volumes: 8 | - ./config:/config # 冒号左边请修改为你想保存配置的路径 9 | - /你的媒体目录:/你想设置的容器内能见到的目录 # 媒体目录,多个目录需要分别映射进来,需要满足配置文件说明中的要求 10 | environment: 11 | - PUID=0 # 想切换为哪个用户来运行程序,该用户的uid 12 | - PGID=0 # 想切换为哪个用户来运行程序,该用户的gid 13 | - UMASK=000 # 掩码权限,默认000,可以考虑设置为022 14 | - NASTOOL_AUTO_UPDATE=false # 如需在启动容器时自动升级程程序请设置为true 15 | #- REPO_URL=https://ghproxy.com/https://github.com/hsuyelin/nas-tools.git # 当你访问github网络很差时,可以考虑解释本行注释 16 | restart: always 17 | network_mode: bridge 18 | hostname: nas-tools 19 | container_name: nas-tools -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/init-010-update/type: -------------------------------------------------------------------------------- 1 | oneshot -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/init-010-update/up: -------------------------------------------------------------------------------- 1 | /etc/s6-overlay/s6-rc.d/init-010-update/run -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/init-020-fixuser/dependencies.d/init-010-update: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-020-fixuser/dependencies.d/init-010-update -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/init-020-fixuser/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | # shellcheck shell=bash 3 | 4 | function INFO() { 5 | echo -e "[\033[32mINFO\033[0m] ${1}" 6 | } 7 | function ERROR() { 8 | echo -e "[\033[31mERROR\033[0m] ${1}" 9 | } 10 | function WARN() { 11 | echo -e "[\033[33mWARN\033[0m] ${1}" 12 | } 13 | 14 | INFO "以PUID=${PUID},PGID=${PGID}的身份启动程序..." 15 | 16 | # 更改 nt userid 和 groupid 17 | groupmod -o -g "$PGID" nt 18 | usermod -o -u "$PUID" nt 19 | 20 | # 创建目录、权限设置 21 | if grep -Eqi "Debian" /etc/issue || grep -Eq "Debian" /etc/os-release; then 22 | chown -R nt:nt /usr/bin/chromedriver 23 | fi 24 | chown -R nt:nt "${WORKDIR}" "${HOME}" /config /usr/lib/chromium /etc/hosts -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/init-020-fixuser/type: -------------------------------------------------------------------------------- 1 | oneshot -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/init-020-fixuser/up: -------------------------------------------------------------------------------- 1 | /etc/s6-overlay/s6-rc.d/init-020-fixuser/run -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/svc-nastools/dependencies.d/init-020-fixuser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-nastools/dependencies.d/init-020-fixuser -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/svc-nastools/finish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | # shellcheck shell=bash 3 | 4 | pkill -f 'python3 run.py' -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/svc-nastools/notification-fd: -------------------------------------------------------------------------------- 1 | 3 -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/svc-nastools/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | # shellcheck shell=bash 3 | 4 | umask ${UMASK} 5 | 6 | if [ -f ${NASTOOL_CONFIG} ]; then 7 | NT_PORT=$(yq '.app.web_port' /config/config.yaml) 8 | else 9 | NT_PORT=3000 10 | fi 11 | 12 | exec \ 13 | s6-notifyoncheck -d -n 300 -w 1000 -c "nc -z localhost ${NT_PORT}" \ 14 | cd ${WORKDIR} s6-setuidgid nt python3 run.py -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/svc-nastools/type: -------------------------------------------------------------------------------- 1 | longrun -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/svc-redis/dependencies.d/init-020-fixuser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-redis/dependencies.d/init-020-fixuser -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/svc-redis/finish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | # shellcheck shell=bash 3 | 4 | pkill -f 'redis-server' -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/svc-redis/notification-fd: -------------------------------------------------------------------------------- 1 | 3 -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/svc-redis/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | # shellcheck shell=bash 3 | 4 | function __debug { 5 | 6 | exec \ 7 | s6-notifyoncheck -d -n 300 -w 1000 -c "nc -z localhost 6379" \ 8 | s6-setuidgid root $(which redis-server) 9 | 10 | } 11 | 12 | function __false { 13 | 14 | exec \ 15 | s6-notifyoncheck -d -n 300 -w 1000 -c "nc -z localhost 6379" \ 16 | s6-setuidgid root $(which redis-server) > /dev/null 2>&1 17 | 18 | } 19 | 20 | if [ -f ${NASTOOL_CONFIG} ]; then 21 | NT_LOG=$(yq '.app.loglevel' /config/config.yaml) 22 | if [[ "${NT_LOG}" == "debug" ]]; then 23 | __debug 24 | else 25 | __false 26 | fi 27 | else 28 | __false 29 | fi 30 | -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/svc-redis/type: -------------------------------------------------------------------------------- 1 | longrun -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/init-010-update: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/init-010-update -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/init-020-fixuser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/init-020-fixuser -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-nastools: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-nastools -------------------------------------------------------------------------------- /docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-redis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-redis -------------------------------------------------------------------------------- /docker/volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/docker/volume.png -------------------------------------------------------------------------------- /package/builder/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.11-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/hsuyelin/nas-tools --recurse-submodule /nas-tools 11 | WORKDIR /nas-tools 12 | RUN pip install -r package/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 ./scripts/. ${py_site_packages}/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.11-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/hsuyelin/nas-tools --recurse-submodule /nas-tools 12 | WORKDIR /nas-tools 13 | RUN pip install -r package/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 ./scripts/. ${py_site_packages}/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 -------------------------------------------------------------------------------- /package/nas-tools.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/package/nas-tools.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 | -------------------------------------------------------------------------------- /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 |出错啦!
19 |20 | 没有找到这个页面,请检查是不是输错地址了... 21 |
22 | 28 |出错啦!
19 |20 | 系统出错了,请检查运行日志看看吧... 21 |
22 | 28 |{{ title }}
9 |10 | {{ text }} 11 |
12 |{{ title }}
25 |26 | {{ text }} 27 |
28 |{{ title }}
41 |42 | {{ text }} 43 |
44 |57 | 正在加载 58 |
59 |