├── .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 |
18 |

{{ diagram.title }}

19 |
{{ diagram.text }}
20 |
21 | {{ diagram.svg }} 22 |
23 |
24 | {% endfor %} 25 | 26 | 27 | -------------------------------------------------------------------------------- /package/rely/upx.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/package/rely/upx.exe -------------------------------------------------------------------------------- /package/trayicon.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import webbrowser 4 | 5 | import wx 6 | import wx.adv 7 | 8 | 9 | class Balloon(wx.adv.TaskBarIcon): 10 | ICON = os.path.dirname(__file__).replace("package", "") + "nas-tools.ico" 11 | 12 | def __init__(self, homepage, log_path): 13 | wx.adv.TaskBarIcon.__init__(self) 14 | self.SetIcon(wx.Icon(self.ICON)) 15 | self.Bind(wx.adv.EVT_TASKBAR_LEFT_DCLICK, self.OnTaskBarLeftDClick) 16 | self.homepage = homepage 17 | self.log_path = log_path 18 | 19 | # Menu数据 20 | def setMenuItemData(self): 21 | return ("Log", self.Onlog), ("Close", self.OnClose) 22 | 23 | # 创建菜单 24 | def CreatePopupMenu(self): 25 | menu = wx.Menu() 26 | for itemName, itemHandler in self.setMenuItemData(): 27 | if not itemName: # itemName为空就添加分隔符 28 | menu.AppendSeparator() 29 | continue 30 | menuItem = wx.MenuItem(None, wx.ID_ANY, text=itemName, kind=wx.ITEM_NORMAL) # 创建菜单项 31 | menu.Append(menuItem) # 将菜单项添加到菜单 32 | self.Bind(wx.EVT_MENU, itemHandler, menuItem) 33 | return menu 34 | 35 | def OnTaskBarLeftDClick(self, event): 36 | webbrowser.open(self.homepage) 37 | 38 | def Onlog(self, event): 39 | os.startfile(self.log_path) 40 | 41 | @staticmethod 42 | def OnClose(event): 43 | exe_name = os.path.basename(sys.executable) 44 | os.system('taskkill /F /IM ' + exe_name) 45 | 46 | 47 | class TrayIcon(wx.Frame): 48 | def __init__(self, homepage, log_path): 49 | app = wx.App() 50 | wx.Frame.__init__(self, None) 51 | self.taskBarIcon = Balloon(homepage, log_path) 52 | webbrowser.open(homepage) 53 | self.Hide() 54 | app.MainLoop() 55 | 56 | 57 | class NullWriter: 58 | softspace = 0 59 | encoding = 'UTF-8' 60 | 61 | def write(*args): 62 | pass 63 | 64 | def flush(*args): 65 | pass 66 | 67 | # Some packages are checking if stdout/stderr is available (e.g., youtube-dl). For details, see #1883. 68 | def isatty(self): 69 | return False 70 | -------------------------------------------------------------------------------- /package_list.txt: -------------------------------------------------------------------------------- 1 | bash 2 | chromium-chromedriver 3 | curl 4 | ffmpeg 5 | fuse3 6 | git 7 | inotify-tools 8 | netcat-openbsd 9 | procps-ng 10 | redis 11 | s6-overlay 12 | shadow 13 | sudo 14 | tzdata 15 | wget 16 | xvfb 17 | yq 18 | zip -------------------------------------------------------------------------------- /package_list_debian.txt: -------------------------------------------------------------------------------- 1 | apt-transport-https 2 | apt-utils 3 | bash 4 | ca-certificates 5 | chromium 6 | chromium-driver 7 | curl 8 | dumb-init 9 | ffmpeg 10 | fuse3 11 | git 12 | inotify-tools 13 | locales 14 | musl-dev 15 | netcat 16 | procps 17 | redis 18 | sudo 19 | tzdata 20 | wget 21 | xvfb 22 | zip -------------------------------------------------------------------------------- /scripts/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade() -> None: 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade() -> None: 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /scripts/sqls/stop_all_service.sql: -------------------------------------------------------------------------------- 1 | UPDATE SITE_BRUSH_TASK SET STATE = 'N' WHERE 1; 2 | UPDATE TORRENT_REMOVE_TASK SET ENABLED = 0 WHERE 1; -------------------------------------------------------------------------------- /scripts/sqls/update_downloader.sql: -------------------------------------------------------------------------------- 1 | UPDATE DOWNLOADER SET MATCH_PATH = 0 WHERE MATCH_PATH IS NULL -------------------------------------------------------------------------------- /scripts/sqls/update_subscribe.sql: -------------------------------------------------------------------------------- 1 | UPDATE RSS_MOVIES SET DOWNLOAD_SETTING = null WHERE DOWNLOAD_SETTING = -1; 2 | UPDATE RSS_TVS SET DOWNLOAD_SETTING = null WHERE DOWNLOAD_SETTING = -1; 3 | -------------------------------------------------------------------------------- /scripts/sqls/update_systemdict.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM SYSTEM_DICT WHERE TYPE like '刷流%'; -------------------------------------------------------------------------------- /scripts/sqls/update_userpris.sql: -------------------------------------------------------------------------------- 1 | UPDATE main.CONFIG_USERS SET PRIS = replace(PRIS, '推荐', '探索') WHERE 1 -------------------------------------------------------------------------------- /scripts/sqls/update_userrss.sql: -------------------------------------------------------------------------------- 1 | UPDATE CONFIG_USER_RSS SET PROCESS_COUNT = '0' WHERE PROCESS_COUNT is null -------------------------------------------------------------------------------- /scripts/versions/13a58bd5311f_1_2_2.py: -------------------------------------------------------------------------------- 1 | """1.2.2 2 | 3 | Revision ID: 13a58bd5311f 4 | Revises: 69508d1aed24 5 | Create Date: 2023-04-04 08:49:43.453901 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '13a58bd5311f' 14 | down_revision = '69508d1aed24' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | # 1.2.2 22 | try: 23 | with op.batch_alter_table("RSS_TVS") as batch_op: 24 | batch_op.add_column(sa.Column('FILTER_INCLUDE', sa.Text, nullable=True)) 25 | batch_op.add_column(sa.Column('FILTER_EXCLUDE', sa.Text, nullable=True)) 26 | except Exception as e: 27 | pass 28 | try: 29 | with op.batch_alter_table("RSS_MOVIES") as batch_op: 30 | batch_op.add_column(sa.Column('FILTER_INCLUDE', sa.Text, nullable=True)) 31 | batch_op.add_column(sa.Column('FILTER_EXCLUDE', sa.Text, nullable=True)) 32 | except Exception as e: 33 | pass 34 | # ### end Alembic commands ### 35 | 36 | 37 | def downgrade() -> None: 38 | pass 39 | -------------------------------------------------------------------------------- /scripts/versions/1f5cc26cdd3d_1_2_3.py: -------------------------------------------------------------------------------- 1 | """1.2.3 2 | 3 | Revision ID: 1f5cc26cdd3d 4 | Revises: 13a58bd5311f 5 | Create Date: 2023-04-07 08:23:05.282129 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '1f5cc26cdd3d' 14 | down_revision = '13a58bd5311f' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | # 1.2.2 22 | try: 23 | with op.batch_alter_table("SITE_BRUSH_TASK") as batch_op: 24 | batch_op.add_column(sa.Column('SAVEPATH', sa.Text, nullable=True)) 25 | except Exception as e: 26 | pass 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade() -> None: 31 | pass 32 | -------------------------------------------------------------------------------- /scripts/versions/69508d1aed24_1_2_1.py: -------------------------------------------------------------------------------- 1 | """1.2.1 2 | 3 | Revision ID: 69508d1aed24 4 | Revises: 6abeaa9ece15 5 | Create Date: 2023-03-24 11:12:51.646014 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '69508d1aed24' 14 | down_revision = '6abeaa9ece15' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | # 1.2.1 22 | try: 23 | with op.batch_alter_table("SITE_BRUSH_TASK") as batch_op: 24 | batch_op.add_column(sa.Column('RSSURL', sa.Text, nullable=True)) 25 | except Exception as e: 26 | pass 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade() -> None: 31 | pass 32 | -------------------------------------------------------------------------------- /scripts/versions/702b7666a634_1_2_5.py: -------------------------------------------------------------------------------- 1 | """1.2.5 2 | 3 | Revision ID: 702b7666a634 4 | Revises: ae61cfa6ada6 5 | Create Date: 2023-04-14 13:08:44.689878 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '702b7666a634' 14 | down_revision = 'ae61cfa6ada6' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | try: 22 | with op.batch_alter_table("DOWNLOAD_SETTING") as batch_op: 23 | batch_op.drop_column('CONTENT_LAYOUT') 24 | except Exception as e: 25 | pass 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade() -> None: 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | pass 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /scripts/versions/7c14267ffbe4_1_2_8.py: -------------------------------------------------------------------------------- 1 | """1.2.8 2 | 3 | Revision ID: 7c14267ffbe4 4 | Revises: a19a48dbb41b 5 | Create Date: 2023-05-13 11:42:58.215215 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '7c14267ffbe4' 14 | down_revision = 'a19a48dbb41b' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | try: 22 | with op.batch_alter_table("SITE_BRUSH_TASK") as batch_op: 23 | batch_op.add_column(sa.Column('UP_LIMIT', sa.Text, nullable=True)) 24 | batch_op.add_column(sa.Column('DL_LIMIT', sa.Text, nullable=True)) 25 | except Exception as e: 26 | pass 27 | 28 | 29 | def downgrade() -> None: 30 | pass 31 | -------------------------------------------------------------------------------- /scripts/versions/a19a48dbb41b_1_2_7.py: -------------------------------------------------------------------------------- 1 | """1.2.7 2 | 3 | Revision ID: a19a48dbb41b 4 | Revises: d68a85a8f10d 5 | Create Date: 2023-05-09 14:44:03.251571 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'a19a48dbb41b' 14 | down_revision = 'd68a85a8f10d' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | try: 22 | op.create_index(op.f('ix_TRANSFER_HISTORY_DATE'), 'TRANSFER_HISTORY', ['DATE'], unique=False) 23 | except Exception as e: 24 | pass 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade() -> None: 29 | pass 30 | -------------------------------------------------------------------------------- /scripts/versions/ae61cfa6ada6_1_2_4.py: -------------------------------------------------------------------------------- 1 | """1.2.4 2 | 3 | Revision ID: ae61cfa6ada6 4 | Revises: 1f5cc26cdd3d 5 | Create Date: 2023-04-11 10:24:45.522668 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'ae61cfa6ada6' 13 | down_revision = '1f5cc26cdd3d' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade() -> None: 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | try: 21 | with op.batch_alter_table("DOWNLOAD_HISTORY") as batch_op: 22 | batch_op.add_column(sa.Column('SE', sa.Text, nullable=True)) 23 | batch_op.add_column(sa.Column('SAVE_PATH', sa.Text, nullable=True)) 24 | batch_op.create_index('ix_DOWNLOAD_HISTORY_SAVE_PATH', ['SAVE_PATH']) 25 | except Exception as e: 26 | pass 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade() -> None: 31 | pass 32 | -------------------------------------------------------------------------------- /scripts/versions/d68a85a8f10d_1_2_6.py: -------------------------------------------------------------------------------- 1 | """1.2.6 2 | 3 | Revision ID: d68a85a8f10d 4 | Revises: 702b7666a634 5 | Create Date: 2023-04-16 14:03:56.871650 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'd68a85a8f10d' 14 | down_revision = '702b7666a634' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | try: 22 | with op.batch_alter_table("CONFIG_SYNC_PATHS") as batch_op: 23 | batch_op.add_column(sa.Column('COMPATIBILITY', sa.Integer, nullable=True)) 24 | except Exception as e: 25 | pass 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade() -> None: 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | pass 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /scripts/versions/eb3437042cc8_1_3_1.py: -------------------------------------------------------------------------------- 1 | """1.3.1 2 | 3 | Revision ID: eb3437042cc8 4 | Revises: ff1b04a637f8 5 | Create Date: 2023-11-22 17:07:42.765426 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'eb3437042cc8' 14 | down_revision = 'ff1b04a637f8' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | try: 22 | with op.batch_alter_table('DOWNLOADER') as batch_op: 23 | batch_op.add_column(sa.Column('ONLY_NASTOOL', sa.Integer(), nullable=True)) 24 | except Exception as e: 25 | pass 26 | try: 27 | with op.batch_alter_table('TORRENT_REMOVE_TASK') as batch_op: 28 | batch_op.add_column(sa.Column('ONLY_NASTOOL', sa.Integer(), nullable=True)) 29 | except Exception as e: 30 | pass 31 | # ### end Alembic commands ### 32 | 33 | 34 | def downgrade() -> None: 35 | # ### commands auto generated by Alembic - please adjust! ### 36 | pass 37 | # ### end Alembic commands ### 38 | -------------------------------------------------------------------------------- /scripts/versions/ff1b04a637f8_1_3_0.py: -------------------------------------------------------------------------------- 1 | """1.3.0 2 | 3 | Revision ID: ff1b04a637f8 4 | Revises: 7c14267ffbe4 5 | Create Date: 2023-09-17 09:35:42.773359 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'ff1b04a637f8' 14 | down_revision = '7c14267ffbe4' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | try: 22 | with op.batch_alter_table('CONFIG_SYNC_PATHS') as batch_op: 23 | batch_op.add_column(sa.Column('LOCATING', sa.Integer(), nullable=True)) 24 | except Exception as e: 25 | pass 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade() -> None: 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | pass 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/tests/__init__.py -------------------------------------------------------------------------------- /tests/cases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/tests/cases/__init__.py -------------------------------------------------------------------------------- /tests/run.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from tests.test_metainfo import MetaInfoTest 4 | 5 | if __name__ == '__main__': 6 | suite = unittest.TestSuite() 7 | # 测试名称识别 8 | suite.addTest(MetaInfoTest('test_metainfo')) 9 | 10 | # 运行测试 11 | runner = unittest.TextTestRunner() 12 | runner.run(suite) 13 | -------------------------------------------------------------------------------- /tests/test_metainfo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from unittest import TestCase 4 | 5 | from app.media.meta import MetaInfo 6 | from tests.cases.meta_cases import meta_cases 7 | 8 | 9 | class MetaInfoTest(TestCase): 10 | def setUp(self) -> None: 11 | pass 12 | 13 | def tearDown(self) -> None: 14 | pass 15 | 16 | def test_metainfo(self): 17 | for info in meta_cases: 18 | if not info.get("title"): 19 | continue 20 | meta_info = MetaInfo(title=info.get("title"), subtitle=info.get("subtitle")) 21 | target = { 22 | "type": meta_info.type.value, 23 | "cn_name": meta_info.cn_name or "", 24 | "en_name": meta_info.en_name or "", 25 | "year": meta_info.year or "", 26 | "part": meta_info.part or "", 27 | "season": meta_info.get_season_string(), 28 | "episode": meta_info.get_episode_string(), 29 | "restype": meta_info.get_edtion_string(), 30 | "pix": meta_info.resource_pix or "", 31 | "video_codec": meta_info.video_encode or "", 32 | "audio_codec": meta_info.audio_encode or "" 33 | } 34 | self.assertEqual(target, info.get("target")) 35 | -------------------------------------------------------------------------------- /third_party.txt: -------------------------------------------------------------------------------- 1 | feapder -------------------------------------------------------------------------------- /third_party/feapder/.gitignore: -------------------------------------------------------------------------------- 1 | files/* 2 | .DS_Store 3 | .idea/* 4 | */.idea/* 5 | venv/* 6 | venv2/* 7 | *.pyc 8 | *test.py 9 | *.log 10 | **/proxy_file 11 | build/ 12 | dist/ 13 | *.egg-info/ 14 | .vscode/ 15 | media/ 16 | .MWebMetaData/ 17 | push.sh 18 | assets/ -------------------------------------------------------------------------------- /third_party/feapder/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Modifications: 4 | 5 | Copyright (c) 2020 Boris 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /third_party/feapder/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | 4 | include feapder/requirements.txt 5 | include feapder/VERSION 6 | 7 | recursive-include feapder/utils/js * 8 | recursive-include feapder/templates * 9 | recursive-include tests * 10 | 11 | global-exclude __pycache__ *.py[cod] -------------------------------------------------------------------------------- /third_party/feapder/feapder/VERSION: -------------------------------------------------------------------------------- 1 | 1.8.9-beta1 -------------------------------------------------------------------------------- /third_party/feapder/feapder/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2020/4/21 10:41 PM 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | import os 11 | import re 12 | import sys 13 | 14 | sys.path.insert(0, re.sub(r"([\\/]items$)|([\\/]spiders$)", "", os.getcwd())) 15 | 16 | __all__ = [ 17 | "AirSpider", 18 | "Spider", 19 | "TaskSpider", 20 | "BatchSpider", 21 | "BaseParser", 22 | "TaskParser", 23 | "BatchParser", 24 | "Request", 25 | "Response", 26 | "Item", 27 | "UpdateItem", 28 | "ArgumentParser", 29 | ] 30 | 31 | from feapder.core.spiders import AirSpider, Spider, TaskSpider, BatchSpider 32 | from feapder.core.base_parser import BaseParser, TaskParser, BatchParser 33 | from feapder.network.request import Request 34 | from feapder.network.response import Response 35 | from feapder.network.item import Item, UpdateItem 36 | from feapder.utils.custom_argparse import ArgumentParser 37 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/buffer/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on 2020/4/23 12:09 AM 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | ''' -------------------------------------------------------------------------------- /third_party/feapder/feapder/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/third_party/feapder/feapder/commands/__init__.py -------------------------------------------------------------------------------- /third_party/feapder/feapder/commands/create/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "CreateProject", 3 | "CreateSpider", 4 | "CreateItem", 5 | "CreateInit", 6 | "CreateJson", 7 | "CreateTable", 8 | "CreateCookies", 9 | "CreateSetting", 10 | "CreateParams", 11 | ] 12 | 13 | from .create_table import CreateTable 14 | from .create_json import CreateJson 15 | from .create_spider import CreateSpider 16 | from .create_init import CreateInit 17 | from .create_item import CreateItem 18 | from .create_project import CreateProject 19 | from .create_cookies import CreateCookies 20 | from .create_setting import CreateSetting 21 | from .create_params import CreateParams 22 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/commands/create/create_cookies.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2021/4/25 10:22 上午 4 | --------- 5 | @summary: 将浏览器的cookie转为request的cookie 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | 11 | import json 12 | 13 | import pyperclip 14 | 15 | from feapder.utils.tools import get_cookies_from_str, print_pretty 16 | 17 | 18 | class CreateCookies: 19 | def get_data(self): 20 | """ 21 | @summary: 从剪切板中读取内容 22 | --------- 23 | --------- 24 | @result: 25 | """ 26 | input("请复制浏览器cookie (列表或字符串格式), 复制后按任意键读取剪切板内容\n") 27 | 28 | text = pyperclip.paste() 29 | print(text + "\n") 30 | 31 | return text 32 | 33 | def create(self): 34 | data = self.get_data() 35 | cookies = {} 36 | try: 37 | data_json = json.loads(data) 38 | 39 | for data in data_json: 40 | cookies[data.get("name")] = data.get("value") 41 | 42 | except: 43 | cookies = get_cookies_from_str(data) 44 | 45 | print_pretty(cookies) 46 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/commands/create/create_init.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2018-08-28 17:38:43 4 | --------- 5 | @summary: 创建__init__.py 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | 11 | from feapder.utils.tools import dumps_json 12 | 13 | 14 | class CreateInit: 15 | def create(self): 16 | __all__ = [] 17 | 18 | import os 19 | 20 | path = os.getcwd() 21 | for file in os.listdir(path): 22 | if file.endswith(".py") and not file.startswith("__init__"): 23 | model = file.split(".")[0] 24 | __all__.append(model) 25 | 26 | del os 27 | 28 | with open("__init__.py", "w", encoding="utf-8") as file: 29 | text = "__all__ = %s" % dumps_json(__all__) 30 | file.write(text) 31 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/commands/create/create_json.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2018-08-28 17:38:43 4 | --------- 5 | @summary: 字符串转json 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | 11 | import pyperclip 12 | 13 | import feapder.utils.tools as tools 14 | 15 | 16 | class CreateJson: 17 | def get_data(self): 18 | """ 19 | @summary: 从控制台读取多行 20 | --------- 21 | --------- 22 | @result: 23 | """ 24 | input("请复制需要转换的内容(xxx:xxx格式,支持多行),复制后按任意键读取剪切板内容\n") 25 | 26 | text = pyperclip.paste() 27 | print(text + "\n") 28 | 29 | data = [] 30 | for line in text.split("\n"): 31 | line = line.strip().replace("\t", " " * 4) 32 | if not line: 33 | break 34 | 35 | data.append(line) 36 | 37 | return data 38 | 39 | def create(self, sort_keys=False): 40 | contents = self.get_data() 41 | 42 | json = {} 43 | for content in contents: 44 | content = content.strip() 45 | if not content or content.startswith(":"): 46 | continue 47 | 48 | regex = "([^:\s]*)[:|\s]*(.*)" 49 | 50 | result = tools.get_info(content, regex, fetch_one=True) 51 | if result[0] in json: 52 | json[result[0]] = json[result[0]] + "&" + result[1] 53 | else: 54 | json[result[0]] = result[1].strip() 55 | 56 | print(tools.dumps_json(json, sort_keys=sort_keys)) 57 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/commands/create/create_params.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2021/4/25 10:22 上午 4 | --------- 5 | @summary: 将浏览器的cookie转为request的cookie 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | 11 | import sys 12 | 13 | from feapder.utils.tools import dumps_json 14 | 15 | 16 | class CreateParams: 17 | def get_data(self): 18 | """ 19 | @summary: 从控制台读取多行 20 | --------- 21 | --------- 22 | @result: 23 | """ 24 | print("请输入请求地址") 25 | data = [] 26 | while True: 27 | line = sys.stdin.readline().strip() 28 | if not line: 29 | break 30 | 31 | data.append(line) 32 | 33 | return "".join(data) 34 | 35 | def get_params(self, url): 36 | params_json = {} 37 | params = url.split("?")[-1].split("&") 38 | for param in params: 39 | key_value = param.split("=", 1) 40 | params_json[key_value[0]] = key_value[1] 41 | 42 | return params_json 43 | 44 | def create(self): 45 | data = self.get_data() 46 | 47 | params = self.get_params(data) 48 | url = data.split("?")[0] 49 | 50 | print(f'url = "{url}"') 51 | print(f"params = {dumps_json(params)}") 52 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/commands/create/create_project.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2018-08-28 17:38:43 4 | --------- 5 | @summary: 创建项目 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | 11 | import getpass 12 | import os 13 | import shutil 14 | 15 | import feapder.utils.tools as tools 16 | 17 | 18 | def deal_file_info(file): 19 | file = file.replace("{DATE}", tools.get_current_date()) 20 | file = file.replace("{USER}", os.getenv("FEAPDER_USER") or getpass.getuser()) 21 | 22 | return file 23 | 24 | 25 | class CreateProject: 26 | def copy_callback(self, src, dst, *, follow_symlinks=True): 27 | if src.endswith(".py"): 28 | with open(src, "r", encoding="utf-8") as src_file, open( 29 | dst, "w", encoding="utf8" 30 | ) as dst_file: 31 | content = src_file.read() 32 | content = deal_file_info(content) 33 | dst_file.write(content) 34 | 35 | else: 36 | shutil.copy2(src, dst, follow_symlinks=follow_symlinks) 37 | 38 | def create(self, project_name): 39 | if os.path.exists(project_name): 40 | print("%s 项目已经存在" % project_name) 41 | else: 42 | template_path = os.path.abspath( 43 | os.path.join(__file__, "../../../templates/project_template") 44 | ) 45 | shutil.copytree( 46 | template_path, project_name, copy_function=self.copy_callback 47 | ) 48 | 49 | print("\n%s 项目生成成功" % project_name) 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/commands/create/create_setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2021/4/23 13:20 4 | --------- 5 | @summary: 生成配置文件 6 | --------- 7 | @author: mkdir700 8 | @email: mkdir700@gmail.com 9 | """ 10 | 11 | import os 12 | import shutil 13 | 14 | 15 | class CreateSetting: 16 | def create(self): 17 | if os.path.exists("setting.py"): 18 | confirm = input("配置文件已存在 是否覆盖 (y/n). ") 19 | if confirm != "y": 20 | print("取消覆盖 退出") 21 | return 22 | 23 | template_file_path = os.path.abspath( 24 | os.path.join(__file__, "../../../templates/project_template/setting.py") 25 | ) 26 | shutil.copy(template_file_path, "./", follow_symlinks=False) 27 | print("配置文件生成成功") 28 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/commands/retry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2022/11/18 12:33 PM 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | import argparse 11 | 12 | from feapder.core.handle_failed_items import HandleFailedItems 13 | from feapder.core.handle_failed_requests import HandleFailedRequests 14 | 15 | 16 | def retry_failed_requests(redis_key): 17 | handle_failed_requests = HandleFailedRequests(redis_key) 18 | handle_failed_requests.reput_failed_requests_to_requests() 19 | 20 | 21 | def retry_failed_items(redis_key): 22 | handle_failed_items = HandleFailedItems(redis_key) 23 | handle_failed_items.reput_failed_items_to_db() 24 | handle_failed_items.close() 25 | 26 | 27 | def parse_args(): 28 | parser = argparse.ArgumentParser( 29 | description="重试失败的请求或入库失败的item", 30 | usage="usage: feapder retry [options] [args]", 31 | ) 32 | parser.add_argument( 33 | "-r", 34 | "--request", 35 | help="重试失败的request 如 feapder retry --request ", 36 | metavar="", 37 | ) 38 | parser.add_argument( 39 | "-i", "--item", help="重试失败的item 如 feapder retry --item ", metavar="" 40 | ) 41 | args = parser.parse_args() 42 | return args 43 | 44 | 45 | def main(): 46 | args = parse_args() 47 | if args.request: 48 | retry_failed_requests(args.request) 49 | if args.item: 50 | retry_failed_items(args.item) 51 | 52 | 53 | if __name__ == "__main__": 54 | main() 55 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/core/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on 2020/4/23 12:09 AM 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | ''' -------------------------------------------------------------------------------- /third_party/feapder/feapder/core/handle_failed_requests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2018-08-13 11:43:01 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | import feapder.setting as setting 11 | from feapder.buffer.request_buffer import RequestBuffer 12 | from feapder.db.redisdb import RedisDB 13 | from feapder.network.request import Request 14 | from feapder.utils.log import log 15 | 16 | 17 | class HandleFailedRequests: 18 | def __init__(self, redis_key): 19 | if redis_key.endswith(":z_failed_requests"): 20 | redis_key = redis_key.replace(":z_failed_requests", "") 21 | 22 | self._redisdb = RedisDB() 23 | self._request_buffer = RequestBuffer(redis_key) 24 | 25 | self._table_failed_request = setting.TAB_FAILED_REQUESTS.format( 26 | redis_key=redis_key 27 | ) 28 | 29 | def get_failed_requests(self, count=10000): 30 | failed_requests = self._redisdb.zget(self._table_failed_request, count=count) 31 | failed_requests = [eval(failed_request) for failed_request in failed_requests] 32 | return failed_requests 33 | 34 | def reput_failed_requests_to_requests(self): 35 | log.debug("正在重置失败的requests...") 36 | total_count = 0 37 | while True: 38 | try: 39 | failed_requests = self.get_failed_requests() 40 | if not failed_requests: 41 | break 42 | 43 | for request in failed_requests: 44 | request["retry_times"] = 0 45 | request_obj = Request.from_dict(request) 46 | self._request_buffer.put_request(request_obj) 47 | 48 | total_count += 1 49 | except Exception as e: 50 | log.exception(e) 51 | 52 | self._request_buffer.flush() 53 | 54 | log.debug("重置%s条失败requests为待抓取requests" % total_count) 55 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/core/spiders/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2020/4/22 12:08 AM 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | 11 | __all__ = ["AirSpider", "TaskSpider", "Spider", "BatchSpider"] 12 | 13 | from feapder.core.spiders.air_spider import AirSpider 14 | from feapder.core.spiders.spider import Spider 15 | from feapder.core.spiders.task_spider import TaskSpider 16 | from feapder.core.spiders.batch_spider import BatchSpider 17 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/db/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2020/4/23 12:09 AM 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ -------------------------------------------------------------------------------- /third_party/feapder/feapder/db/memorydb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2020/4/21 11:42 PM 4 | --------- 5 | @summary: 基于内存的队列,代替redis 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | from queue import PriorityQueue 11 | 12 | from feapder import setting 13 | 14 | 15 | class MemoryDB: 16 | def __init__(self): 17 | self.priority_queue = PriorityQueue(maxsize=setting.TASK_MAX_CACHED_SIZE) 18 | 19 | def add(self, item, ignore_max_size=False): 20 | """ 21 | 添加任务 22 | :param item: 数据: 支持小于号比较的类 或者 (priority, item) 23 | :param ignore_max_size: queue满时是否等待,为True时无视队列的maxsize,直接往里塞 24 | :return: 25 | """ 26 | if ignore_max_size: 27 | self.priority_queue._put(item) 28 | self.priority_queue.unfinished_tasks += 1 29 | else: 30 | self.priority_queue.put(item) 31 | 32 | def get(self): 33 | """ 34 | 获取任务 35 | :return: 36 | """ 37 | try: 38 | item = self.priority_queue.get(timeout=1) 39 | return item 40 | except: 41 | return 42 | 43 | def empty(self): 44 | return self.priority_queue.empty() 45 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/dedup/README.md: -------------------------------------------------------------------------------- 1 | # Dedup 2 | 3 | Dedup是feapder大数据去重模块,内置3种去重机制,使用方式一致,可容纳的去重数据量与内存有关。不同于BloomFilter,去重受槽位数量影响,Dedup使用了弹性的去重机制,可容纳海量的数据去重。 4 | 5 | 6 | ## 去重方式 7 | 8 | ### 临时去重 9 | 10 | > 基于redis,支持批量,去重有时效性。去重一万条数据约0.26秒,一亿条数据占用内存约1.43G 11 | 12 | ``` 13 | from feapder.dedup import Dedup 14 | 15 | data = {"xxx": 123, "xxxx": "xxxx"} 16 | datas = ["xxx", "bbb"] 17 | 18 | def test_ExpireFilter(): 19 | dedup = Dedup( 20 | Dedup.ExpireFilter, expire_time=10, redis_url="redis://@localhost:6379/0" 21 | ) 22 | 23 | # 逐条去重 24 | assert dedup.add(data) == 1 25 | assert dedup.get(data) == 1 26 | 27 | # 批量去重 28 | assert dedup.add(datas) == [1, 1] 29 | assert dedup.get(datas) == [1, 1] 30 | ``` 31 | 32 | 33 | ### 内存去重 34 | 35 | > 基于内存,支持批量。去重一万条数据约0.5秒,一亿条数据占用内存约285MB 36 | 37 | ``` 38 | from feapder.dedup import Dedup 39 | 40 | data = {"xxx": 123, "xxxx": "xxxx"} 41 | datas = ["xxx", "bbb"] 42 | 43 | def test_MemoryFilter(): 44 | dedup = Dedup(Dedup.MemoryFilter) # 表名为test 历史数据3秒有效期 45 | 46 | # 逐条去重 47 | assert dedup.add(data) == 1 48 | assert dedup.get(data) == 1 49 | 50 | # 批量去重 51 | assert dedup.add(datas) == [1, 1] 52 | assert dedup.get(datas) == [1, 1] 53 | ``` 54 | 55 | ### 永久去重 56 | 57 | > 基于redis,支持批量,永久去重。 去重一万条数据约3.5秒,一亿条数据占用内存约285MB 58 | 59 | from feapder.dedup import Dedup 60 | 61 | datas = { 62 | "xxx": xxx, 63 | "xxxx": "xxxx", 64 | } 65 | 66 | dedup = Dedup() 67 | 68 | print(dedup) # 69 | print(dedup.add(datas)) # 0 不存在 70 | print(dedup.get(datas)) # 1 存在 71 | 72 | ## 过滤数据 73 | 74 | Dedup可以通过如下方法,过滤掉已存在的数据 75 | 76 | 77 | ```python 78 | from feapder.dedup import Dedup 79 | 80 | def test_filter(): 81 | dedup = Dedup(Dedup.BloomFilter, redis_url="redis://@localhost:6379/0") 82 | 83 | # 制造已存在数据 84 | datas = ["xxx", "bbb"] 85 | dedup.add(datas) 86 | 87 | # 过滤掉已存在数据 "xxx", "bbb" 88 | datas = ["xxx", "bbb", "ccc"] 89 | dedup.filter_exist_data(datas) 90 | assert datas == ["ccc"] 91 | ``` 92 | 93 | 94 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/dedup/basefilter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2022/9/21 11:17 AM 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | import abc 11 | from typing import List, Union 12 | 13 | 14 | class BaseFilter: 15 | @abc.abstractmethod 16 | def add( 17 | self, keys: Union[List[str], str], *args, **kwargs 18 | ) -> Union[List[bool], bool]: 19 | """ 20 | 21 | Args: 22 | keys: list / 单个值 23 | *args: 24 | **kwargs: 25 | 26 | Returns: 27 | list / 单个值 (如果数据已存在 返回 0 否则返回 1, 可以理解为是否添加成功) 28 | """ 29 | pass 30 | 31 | @abc.abstractmethod 32 | def get(self, keys: Union[List[str], str]) -> Union[List[bool], bool]: 33 | """ 34 | 检查数据是否存在 35 | Args: 36 | keys: list / 单个值 37 | 38 | Returns: 39 | list / 单个值 (如果数据已存在 返回 1 否则返回 0) 40 | """ 41 | pass 42 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/dedup/litefilter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2022/9/21 11:28 AM 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | from typing import List, Union, Set 11 | 12 | from feapder.dedup.basefilter import BaseFilter 13 | 14 | 15 | class LiteFilter(BaseFilter): 16 | def __init__(self): 17 | self.datas: Set[str] = set() 18 | 19 | def add( 20 | self, keys: Union[List[str], str], *args, **kwargs 21 | ) -> Union[List[int], int]: 22 | """ 23 | 24 | Args: 25 | keys: list / 单个值 26 | *args: 27 | **kwargs: 28 | 29 | Returns: 30 | list / 单个值 (如果数据已存在 返回 0 否则返回 1, 可以理解为是否添加成功) 31 | """ 32 | if isinstance(keys, list): 33 | is_add = [] 34 | for key in keys: 35 | if key not in self.datas: 36 | self.datas.add(key) 37 | is_add.append(1) 38 | else: 39 | is_add.append(0) 40 | else: 41 | if keys not in self.datas: 42 | is_add = 1 43 | self.datas.add(keys) 44 | else: 45 | is_add = 0 46 | return is_add 47 | 48 | def get(self, keys: Union[List[str], str]) -> Union[List[int], int]: 49 | """ 50 | 检查数据是否存在 51 | Args: 52 | keys: list / 单个值 53 | 54 | Returns: 55 | list / 单个值 (如果数据已存在 返回 1 否则返回 0) 56 | """ 57 | if isinstance(keys, list): 58 | temp_set = set() 59 | is_exist = [] 60 | for key in keys: 61 | # 数据本身重复或者数据在去重库里 62 | if key in temp_set or key in self.datas: 63 | is_exist.append(1) 64 | else: 65 | is_exist.append(0) 66 | temp_set.add(key) 67 | 68 | return is_exist 69 | else: 70 | return int(keys in self.datas) 71 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/network/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/third_party/feapder/feapder/network/__init__.py -------------------------------------------------------------------------------- /third_party/feapder/feapder/network/downloader/__init__.py: -------------------------------------------------------------------------------- 1 | from ._requests import RequestsDownloader 2 | from ._requests import RequestsSessionDownloader 3 | 4 | # 下面是非必要依赖 5 | try: 6 | from ._selenium import SeleniumDownloader 7 | except ModuleNotFoundError: 8 | pass 9 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/network/downloader/_requests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2022/4/10 5:57 下午 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | 11 | import requests 12 | from requests.adapters import HTTPAdapter 13 | 14 | from feapder.network.downloader.base import Downloader 15 | from feapder.network.response import Response 16 | 17 | 18 | class RequestsDownloader(Downloader): 19 | def download(self, request) -> Response: 20 | response = requests.request( 21 | request.method, request.url, **request.requests_kwargs 22 | ) 23 | response = Response(response) 24 | return response 25 | 26 | 27 | class RequestsSessionDownloader(Downloader): 28 | session = None 29 | 30 | @property 31 | def _session(self): 32 | if not self.__class__.session: 33 | self.__class__.session = requests.Session() 34 | # pool_connections – 缓存的 urllib3 连接池个数 pool_maxsize – 连接池中保存的最大连接数 35 | http_adapter = HTTPAdapter(pool_connections=1000, pool_maxsize=1000) 36 | # 任何使用该session会话的 HTTP 请求,只要其 URL 是以给定的前缀开头,该传输适配器就会被使用到。 37 | self.__class__.session.mount("http", http_adapter) 38 | 39 | return self.__class__.session 40 | 41 | def download(self, request) -> Response: 42 | response = self._session.request( 43 | request.method, request.url, **request.requests_kwargs 44 | ) 45 | response = Response(response) 46 | return response 47 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/network/downloader/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from abc import ABC 3 | 4 | from feapder.network.response import Response 5 | 6 | 7 | class Downloader: 8 | @abc.abstractmethod 9 | def download(self, request) -> Response: 10 | """ 11 | 12 | Args: 13 | request: feapder.Request 14 | 15 | Returns: feapder.Response 16 | 17 | """ 18 | raise NotImplementedError 19 | 20 | def close(self, response: Response): 21 | pass 22 | 23 | 24 | class RenderDownloader(Downloader, ABC): 25 | def put_back(self, driver): 26 | """ 27 | 释放浏览器对象 28 | """ 29 | pass 30 | 31 | def close(self, driver): 32 | """ 33 | 关闭浏览器 34 | """ 35 | pass 36 | 37 | def close_all(self): 38 | """ 39 | 关闭所有浏览器 40 | """ 41 | pass 42 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/network/proxy_pool/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2023/7/25 10:16 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | from .base import BaseProxyPool 11 | from .proxy_pool import ProxyPool 12 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/network/proxy_pool/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2023/7/25 10:03 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | 11 | import abc 12 | 13 | from feapder.utils.log import log 14 | 15 | 16 | class BaseProxyPool: 17 | @abc.abstractmethod 18 | def get_proxy(self): 19 | """ 20 | 获取代理 21 | Returns: 22 | {"http": "xxx", "https": "xxx"} 23 | """ 24 | raise NotImplementedError 25 | 26 | @abc.abstractmethod 27 | def del_proxy(self, proxy): 28 | """ 29 | @summary: 删除代理 30 | --------- 31 | @param proxy: ip:port 32 | """ 33 | raise NotImplementedError 34 | 35 | def tag_proxy(self, **kwargs): 36 | """ 37 | @summary: 标记代理 38 | --------- 39 | @param kwargs: 40 | @return: 41 | """ 42 | log.warning("暂不支持标记代理") 43 | pass 44 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/network/user_pool/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "GuestUserPool", 3 | "GuestUser", 4 | "NormalUserPool", 5 | "NormalUser", 6 | "GoldUserPool", 7 | "GoldUser", 8 | "GoldUserStatus", 9 | ] 10 | 11 | from .gold_user_pool import GoldUserPool, GoldUser, GoldUserStatus 12 | from .guest_user_pool import GuestUserPool, GuestUser 13 | from .normal_user_pool import NormalUserPool, NormalUser 14 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/pipelines/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2021/3/17 10:57 下午 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | 11 | import abc 12 | from typing import Dict, List, Tuple 13 | 14 | 15 | class BasePipeline(metaclass=abc.ABCMeta): 16 | """ 17 | pipeline 是单线程的,批量保存数据的操作,不建议在这里写网络请求代码,如下载图片等 18 | """ 19 | 20 | @abc.abstractmethod 21 | def save_items(self, table, items: List[Dict]) -> bool: 22 | """ 23 | 保存数据 24 | Args: 25 | table: 表名 26 | items: 数据,[{},{},...] 27 | 28 | Returns: 是否保存成功 True / False 29 | 若False,不会将本批数据入到去重库,以便再次入库 30 | 31 | """ 32 | 33 | return True 34 | 35 | def update_items(self, table, items: List[Dict], update_keys=Tuple) -> bool: 36 | """ 37 | 更新数据, 与UpdateItem配合使用,若爬虫中没使用UpdateItem,则可不实现此接口 38 | Args: 39 | table: 表名 40 | items: 数据,[{},{},...] 41 | update_keys: 更新的字段, 如 ("title", "publish_time") 42 | 43 | Returns: 是否更新成功 True / False 44 | 若False,不会将本批数据入到去重库,以便再次入库 45 | 46 | """ 47 | 48 | return True 49 | 50 | def close(self): 51 | """ 52 | 关闭,爬虫结束时调用 53 | Returns: 54 | 55 | """ 56 | pass 57 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/pipelines/console_pipeline.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2021/3/18 12:39 上午 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | 11 | from feapder.pipelines import BasePipeline 12 | from typing import Dict, List, Tuple 13 | from feapder.utils.log import log 14 | 15 | 16 | class ConsolePipeline(BasePipeline): 17 | """ 18 | pipeline 是单线程的,批量保存数据的操作,不建议在这里写网络请求代码,如下载图片等 19 | """ 20 | 21 | def save_items(self, table, items: List[Dict]) -> bool: 22 | """ 23 | 保存数据 24 | Args: 25 | table: 表名 26 | items: 数据,[{},{},...] 27 | 28 | Returns: 是否保存成功 True / False 29 | 若False,不会将本批数据入到去重库,以便再次入库 30 | 31 | """ 32 | log.info("【调试输出】共导出 %s 条数据 到 %s" % (len(items), table)) 33 | return True 34 | 35 | def update_items(self, table, items: List[Dict], update_keys=Tuple) -> bool: 36 | """ 37 | 更新数据 38 | Args: 39 | table: 表名 40 | items: 数据,[{},{},...] 41 | update_keys: 更新的字段, 如 ("title", "publish_time") 42 | 43 | Returns: 是否更新成功 True / False 44 | 若False,不会将本批数据入到去重库,以便再次入库 45 | 46 | """ 47 | log.info("【调试输出】共导出 %s 条数据 到 %s" % (len(items), table)) 48 | return True 49 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/requirements.txt: -------------------------------------------------------------------------------- 1 | better-exceptions>=0.2.2 2 | DBUtils>=2.0 3 | parsel>=1.5.2 4 | PyExecJS>=1.5.1 5 | pymongo>=3.10.1 6 | PyMySQL>=0.9.3 7 | redis>=2.10.6,<4.0.0 8 | requests>=2.22.0 9 | selenium>=3.141.0 10 | bs4>=0.0.1 11 | ipython>=7.14.0 12 | bitarray>=1.5.3 13 | redis-py-cluster>=2.1.0 14 | cryptography>=3.3.2 15 | urllib3>=1.25.8 16 | loguru>=0.5.3 17 | influxdb>=5.3.1 18 | pyperclip>=1.8.2 19 | webdriver-manager>=3.5.3 20 | terminal-layout>=2.1.3 -------------------------------------------------------------------------------- /third_party/feapder/feapder/templates/air_spider_template.tmpl: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on {DATE} 4 | --------- 5 | @summary: 6 | --------- 7 | @author: {USER} 8 | """ 9 | 10 | import feapder 11 | 12 | 13 | class ${spider_name}(feapder.AirSpider): 14 | def start_requests(self): 15 | yield feapder.Request("https://spidertools.cn") 16 | 17 | def parse(self, request, response): 18 | # 提取网站title 19 | print(response.xpath("//title/text()").extract_first()) 20 | # 提取网站描述 21 | print(response.xpath("//meta[@name='description']/@content").extract_first()) 22 | print("网站地址: ", response.url) 23 | 24 | 25 | if __name__ == "__main__": 26 | ${spider_name}().start() -------------------------------------------------------------------------------- /third_party/feapder/feapder/templates/batch_spider_template.tmpl: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on {DATE} 4 | --------- 5 | @summary: 6 | --------- 7 | @author: {USER} 8 | """ 9 | 10 | import feapder 11 | from feapder import ArgumentParser 12 | 13 | 14 | class ${spider_name}(feapder.BatchSpider): 15 | # 自定义数据库,若项目中有setting.py文件,此自定义可删除 16 | __custom_setting__ = dict( 17 | REDISDB_IP_PORTS="localhost:6379", 18 | REDISDB_USER_PASS="", 19 | REDISDB_DB=0, 20 | MYSQL_IP="localhost", 21 | MYSQL_PORT=3306, 22 | MYSQL_DB="", 23 | MYSQL_USER_NAME="", 24 | MYSQL_USER_PASS="", 25 | ) 26 | 27 | def start_requests(self, task): 28 | yield feapder.Request("https://spidertools.cn") 29 | 30 | def parse(self, request, response): 31 | # 提取网站title 32 | print(response.xpath("//title/text()").extract_first()) 33 | # 提取网站描述 34 | print(response.xpath("//meta[@name='description']/@content").extract_first()) 35 | print("网站地址: ", response.url) 36 | 37 | 38 | if __name__ == "__main__": 39 | spider = ${spider_name}( 40 | redis_key="xxx:xxxx", # 分布式爬虫调度信息存储位置 41 | task_table="", # mysql中的任务表 42 | task_keys=["id", "xxx"], # 需要获取任务表里的字段名,可添加多个 43 | task_state="state", # mysql中任务状态字段 44 | batch_record_table="xxx_batch_record", # mysql中的批次记录表 45 | batch_name="xxx(周全)", # 批次名字 46 | batch_interval=7, # 批次周期 天为单位 若为小时 可写 1 / 24 47 | ) 48 | 49 | parser = ArgumentParser(description="${spider_name}爬虫") 50 | 51 | parser.add_argument( 52 | "--start_master", 53 | action="store_true", 54 | help="添加任务", 55 | function=spider.start_monitor_task, 56 | ) 57 | parser.add_argument( 58 | "--start_worker", action="store_true", help="启动爬虫", function=spider.start 59 | ) 60 | 61 | parser.start() 62 | 63 | # 直接启动 64 | # spider.start() # 启动爬虫 65 | # spider.start_monitor_task() # 添加任务 66 | 67 | # 通过命令行启动 68 | # python ${file_name} --start_master # 添加任务 69 | # python ${file_name} --start_worker # 启动爬虫 70 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/templates/item_template.tmpl: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on {DATE} 4 | --------- 5 | @summary: 6 | --------- 7 | @author: {USER} 8 | """ 9 | 10 | from feapder import Item 11 | 12 | 13 | class ${item_name}Item(Item): 14 | """ 15 | This class was generated by feapder 16 | command: feapder create -i ${command} 17 | """ 18 | 19 | __table_name__ = "${table_name}" 20 | 21 | def __init__(self, *args, **kwargs): 22 | ${propertys} 23 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/templates/project_template/CHECK_DATA.md: -------------------------------------------------------------------------------- 1 | # 数据审核 2 | ## 表说明: 3 | 4 | > 表名 含义(更新策略) 5 | 6 | ## 一、准确性 7 | 8 | **字段设计是否满足需求? 表之间的关联字段是否满足要求? (需要人工检查)** 9 | 10 | > 注意:是否设计了自增 id,id 的类型是否设置为 bigint? 11 | > 注意:unique index 是否需要设计? 12 | > 注意:各张表之间是否需要设计关联字段; 13 | 14 | * [ ] 是 15 | * [ ] 否 16 | 17 | **各字段采集内容及存储格式是否满足要求?是否与网页一致?是否有信息缺失?** 18 | 19 | > 备注:可尝试对每个字段进行升降序排列,然后抽样检查; 20 | 21 | **是否考虑了网站同一类数据可能出现的数据格式不一致情况?** 22 | 23 | > 建议:代码对各个字段不做兼容性处理、数据不一致则抛出异常并记录 24 | 25 | * [ ] 是 26 | * [ ] 否 27 | 28 | ## 二、全量性 29 | 30 | **如果是增量采集,是否最早信息和最晚信息都采集了,同时条目总数是否正确;** 31 | **如果是批次采集,是否每个批次都有?** 32 | 33 | >备注:需要去网页端评估单个批次的总量; 34 | >参考sql语句:SELECT count(1), batch_date from [table_name] GROUP BY batch_date; 35 | 36 | **如果与另外一张表有关联关系,是否信息关联完整?** 37 | 38 | ## 三、稳定性 39 | 40 | * [ ] 是否能够长期稳定采集? 41 | * [ ] 是否加IP代理? 42 | * [ ] 是否支持断点续跑? 43 | * [ ] 是否能确保按时启动,定期采集? 44 | * [ ] 是否已开启报警? 45 | 46 | ## 四、采集频次、类型、存储方式 47 | 48 | * [ ] 采集频次是否满足要求? 49 | * [ ] 采集类型是否满足要求:增量采集 or 批次采集? 50 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/templates/project_template/README.md: -------------------------------------------------------------------------------- 1 | # xxx爬虫文档 2 | ## 调研 3 | 4 | ## 数据库设计 5 | 6 | ## 爬虫逻辑 7 | 8 | ## 项目架构 -------------------------------------------------------------------------------- /third_party/feapder/feapder/templates/project_template/items/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/third_party/feapder/feapder/templates/project_template/items/__init__.py -------------------------------------------------------------------------------- /third_party/feapder/feapder/templates/project_template/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on {DATE} 4 | --------- 5 | @summary: 爬虫入口 6 | --------- 7 | @author: {USER} 8 | """ 9 | 10 | from feapder import ArgumentParser 11 | 12 | from spiders import * 13 | 14 | def crawl_xxx(): 15 | """ 16 | AirSpider爬虫 17 | """ 18 | spider = xxx.XXXSpider() 19 | spider.start() 20 | 21 | def crawl_xxx(): 22 | """ 23 | Spider爬虫 24 | """ 25 | spider = xxx.XXXSpider(redis_key="xxx:xxx") 26 | spider.start() 27 | 28 | 29 | def crawl_xxx(args): 30 | """ 31 | BatchSpider爬虫 32 | """ 33 | spider = xxx_spider.XXXSpider( 34 | task_table="", # mysql中的任务表 35 | batch_record_table="", # mysql中的批次记录表 36 | batch_name="xxx(周全)", # 批次名字 37 | batch_interval=7, # 批次时间 天为单位 若为小时 可写 1 / 24 38 | task_keys=["id", "xxx"], # 需要获取任务表里的字段名,可添加多个 39 | redis_key="xxx:xxxx", # redis中存放request等信息的根key 40 | task_state="state", # mysql中任务状态字段 41 | ) 42 | 43 | if args == 1: 44 | spider.start_monitor_task() 45 | elif args == 2: 46 | spider.start() 47 | elif args == 3: 48 | spider.init_task() 49 | 50 | 51 | if __name__ == "__main__": 52 | parser = ArgumentParser(description="xxx爬虫") 53 | 54 | parser.add_argument( 55 | "--crawl_xxx", action="store_true", help="xxx爬虫", function=crawl_xxx 56 | ) 57 | parser.add_argument( 58 | "--crawl_xxx", action="store_true", help="xxx爬虫", function=crawl_xxx 59 | ) 60 | parser.add_argument( 61 | "--crawl_xxx", 62 | type=int, 63 | nargs=1, 64 | help="xxx爬虫", 65 | choices=[1, 2, 3], 66 | function=crawl_xxx, 67 | ) 68 | 69 | parser.start() 70 | 71 | # main.py作为爬虫启动的统一入口,提供命令行的方式启动多个爬虫,若只有一个爬虫,可不编写main.py 72 | # 将上面的xxx修改为自己实际的爬虫名 73 | # 查看运行命令 python main.py --help 74 | # AirSpider与Spider爬虫运行方式 python main.py --crawl_xxx 75 | # BatchSpider运行方式 76 | # 1. 下发任务:python main.py --crawl_xxx 1 77 | # 2. 采集:python main.py --crawl_xxx 2 78 | # 3. 重置任务:python main.py --crawl_xxx 3 79 | 80 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/templates/project_template/spiders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/third_party/feapder/feapder/templates/project_template/spiders/__init__.py -------------------------------------------------------------------------------- /third_party/feapder/feapder/templates/spider_template.tmpl: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on {DATE} 4 | --------- 5 | @summary: 6 | --------- 7 | @author: {USER} 8 | """ 9 | 10 | import feapder 11 | 12 | 13 | class ${spider_name}(feapder.Spider): 14 | # 自定义数据库,若项目中有setting.py文件,此自定义可删除 15 | __custom_setting__ = dict( 16 | REDISDB_IP_PORTS="localhost:6379", REDISDB_USER_PASS="", REDISDB_DB=0 17 | ) 18 | 19 | def start_requests(self): 20 | yield feapder.Request("https://spidertools.cn") 21 | 22 | def parse(self, request, response): 23 | # 提取网站title 24 | print(response.xpath("//title/text()").extract_first()) 25 | # 提取网站描述 26 | print(response.xpath("//meta[@name='description']/@content").extract_first()) 27 | print("网站地址: ", response.url) 28 | 29 | 30 | if __name__ == "__main__": 31 | ${spider_name}(redis_key="xxx:xxx").start() 32 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/templates/update_item_template.tmpl: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on {DATE} 4 | --------- 5 | @summary: 6 | --------- 7 | @author: {USER} 8 | """ 9 | 10 | from feapder import UpdateItem 11 | 12 | 13 | class ${item_name}Item(UpdateItem): 14 | """ 15 | This class was generated by feapder 16 | command: feapder create -i ${command} 17 | """ 18 | 19 | __table_name__ = "${table_name}" 20 | 21 | def __init__(self, *args, **kwargs): 22 | ${propertys} 23 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on 2019/11/5 4:41 PM 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | ''' -------------------------------------------------------------------------------- /third_party/feapder/feapder/utils/custom_argparse.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2018-10-15 14:32:12 4 | --------- 5 | @summary: 封装ArgumentParser, 使其支持function, 调用start自动执行 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | 11 | import argparse 12 | 13 | 14 | class ArgumentParser(argparse.ArgumentParser): 15 | def __init__(self, *args, **kwargs): 16 | self.functions = {} 17 | 18 | super(ArgumentParser, self).__init__(*args, **kwargs) 19 | 20 | def add_argument(self, *args, **kwargs): 21 | function = kwargs.pop("function") if "function" in kwargs else None 22 | key = self._get_optional_kwargs(*args, **kwargs).get("dest") 23 | self.functions[key] = function 24 | 25 | return super(ArgumentParser, self).add_argument(*args, **kwargs) 26 | 27 | def start(self, args=None, namespace=None): 28 | args = self.parse_args(args=args, namespace=namespace) 29 | for key, value in vars(args).items(): # vars() 函数返回对象object的属性和属性值的字典对象 30 | if value not in (None, False): 31 | if callable(self.functions[key]): 32 | if value != True: 33 | if isinstance(value, list) and len(value) == 1: 34 | value = value[0] 35 | self.functions[key](value) 36 | else: 37 | self.functions[key]() 38 | 39 | def run(self, args, values=None): 40 | if args in self.functions: 41 | if values: 42 | self.functions[args](values) 43 | else: 44 | self.functions[args]() 45 | 46 | else: 47 | raise Exception(f"无此方法: {args}") 48 | 49 | 50 | if __name__ == "__main__": 51 | 52 | def test(): 53 | print("test not args func") 54 | 55 | def test2(args): 56 | print("test args func", args) 57 | 58 | parser = ArgumentParser(description="测试") 59 | 60 | parser.add_argument("--test2", type=int, nargs=1, help="(1|2)", function=test2) 61 | parser.add_argument("--test", action="store_true", help="", function=test) 62 | 63 | parser.start() 64 | -------------------------------------------------------------------------------- /third_party/feapder/feapder/utils/webdriver/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2022/9/7 4:39 PM 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris_liu@foxmail.com 9 | """ 10 | from .selenium_driver import SeleniumDriver 11 | from .webdirver import InterceptRequest, InterceptResponse 12 | from .webdriver_pool import WebDriverPool 13 | 14 | # 为了兼容老代码 15 | WebDriver = SeleniumDriver 16 | -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | APP_VERSION = 'v3.4.1' 2 | -------------------------------------------------------------------------------- /web/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/__init__.py -------------------------------------------------------------------------------- /web/backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/backend/__init__.py -------------------------------------------------------------------------------- /web/backend/user.cp310-win_amd64.pyd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/backend/user.cp310-win_amd64.pyd -------------------------------------------------------------------------------- /web/backend/user.cpython-310-aarch64-linux-gnu.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/backend/user.cpython-310-aarch64-linux-gnu.so -------------------------------------------------------------------------------- /web/backend/user.cpython-310-darwin.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/backend/user.cpython-310-darwin.so -------------------------------------------------------------------------------- /web/backend/user.cpython-310-x86_64-linux-gnu.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/backend/user.cpython-310-x86_64-linux-gnu.so -------------------------------------------------------------------------------- /web/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /web/static/components/accordion/index.js: -------------------------------------------------------------------------------- 1 | export * from "./seasons/index.js"; 2 | -------------------------------------------------------------------------------- /web/static/components/card/index.js: -------------------------------------------------------------------------------- 1 | export * from "./normal/index.js"; 2 | export * from "./person/index.js"; -------------------------------------------------------------------------------- /web/static/components/card/normal/placeholder.js: -------------------------------------------------------------------------------- 1 | import { html } from "../../utility/lit-core.min.js"; 2 | import { CustomElement } from "../../utility/utility.js"; 3 | 4 | export class NormalCardPlaceholder extends CustomElement { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | static render_placeholder() { 10 | return html` 11 |
12 |
13 |
14 | `; 15 | } 16 | 17 | render() { 18 | return html` 19 |
20 | ${NormalCardPlaceholder.render_placeholder()} 21 |
22 | `; 23 | } 24 | } 25 | 26 | window.customElements.define("normal-card-placeholder", NormalCardPlaceholder); -------------------------------------------------------------------------------- /web/static/components/card/normal/state.js: -------------------------------------------------------------------------------- 1 | import { LitState } from "../../utility/lit-state.js" 2 | 3 | class CardState extends LitState { 4 | static get stateVars() { 5 | return { 6 | more_id: undefined 7 | }; 8 | } 9 | } 10 | 11 | export const cardState = new CardState(); -------------------------------------------------------------------------------- /web/static/components/card/person/index.js: -------------------------------------------------------------------------------- 1 | import { html } from "../../utility/lit-core.min.js"; 2 | import { CustomElement, Golbal } from "../../utility/utility.js"; 3 | 4 | export class PersonCard extends CustomElement { 5 | 6 | static properties = { 7 | person_id: { attribute: "person-id" }, 8 | person_image: { attribute: "person-image" }, 9 | person_name: { attribute: "person-name" }, 10 | person_role: { attribute: "person-role" }, 11 | lazy: {}, 12 | }; 13 | 14 | constructor() { 15 | super(); 16 | this.lazy = "0"; 17 | } 18 | 19 | render() { 20 | return html` 21 |
22 |
23 |
24 | 31 |
32 |

34 | ${this.person_name} 35 |

36 |
38 | ${this.person_role} 39 |
40 |
41 |
42 | `; 43 | } 44 | 45 | } 46 | 47 | window.customElements.define("person-card", PersonCard); -------------------------------------------------------------------------------- /web/static/components/custom/index.js: -------------------------------------------------------------------------------- 1 | export * from "./img/index.js"; 2 | export * from "./plex-library-img/index.js"; 3 | export * from "./slide/index.js"; -------------------------------------------------------------------------------- /web/static/components/index.js: -------------------------------------------------------------------------------- 1 | // 导入所有组件 2 | const body_div = document.createElement("div"); 3 | [ 4 | "custom/chips/index.html", 5 | ] 6 | .forEach((name) => { 7 | const my_wc = document.createElement("div"); 8 | $(my_wc).load("../static/components/" + name); 9 | body_div.appendChild(my_wc); 10 | }) 11 | document.body.appendChild(body_div); -------------------------------------------------------------------------------- /web/static/components/layout/index.js: -------------------------------------------------------------------------------- 1 | export * from "./navbar/index.js"; 2 | export * from "./searchbar/index.js"; -------------------------------------------------------------------------------- /web/static/components/layout/navbar/button.js: -------------------------------------------------------------------------------- 1 | import { html } from "../../utility/lit-core.min.js"; 2 | import { CustomElement } from "../../utility/utility.js"; 3 | 4 | 5 | export class LayoutNavbarButton extends CustomElement { 6 | render() { 7 | return html` 8 | 11 | `; 12 | } 13 | } 14 | 15 | 16 | window.customElements.define("layout-navbar-button", LayoutNavbarButton); -------------------------------------------------------------------------------- /web/static/components/lit-index.js: -------------------------------------------------------------------------------- 1 | export * from "./custom/index.js"; 2 | export * from "./card/index.js"; 3 | export * from "./page/index.js"; 4 | export * from "./layout/index.js"; 5 | export * from "./plugin/index.js"; 6 | export * from "./accordion/index.js"; 7 | export * from "./cmd-dialog/index.js"; -------------------------------------------------------------------------------- /web/static/components/page/index.js: -------------------------------------------------------------------------------- 1 | export * from "./discovery/index.js"; 2 | export * from "./mediainfo/index.js"; 3 | export * from "./person/index.js"; -------------------------------------------------------------------------------- /web/static/components/plugin/index.js: -------------------------------------------------------------------------------- 1 | export * from "./modal/index.js"; -------------------------------------------------------------------------------- /web/static/css/nprogress.css: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: none; 4 | } 5 | 6 | #nprogress .bar { 7 | background: var(--tblr-primary) !important; 8 | position: fixed; 9 | z-index: 1031; 10 | top: calc(env(safe-area-inset-top) + var(--safe-area-inset-top)); 11 | left: 0; 12 | width: 100%; 13 | height: 2px; 14 | } 15 | 16 | /* Fancy blur effect */ 17 | #nprogress .peg { 18 | display: block; 19 | position: absolute; 20 | right: 0; 21 | width: 5px; 22 | height: 100%; 23 | box-shadow: 0 0 10px var(--tblr-primary), 0 0 5px var(--tblr-primary); 24 | opacity: 1.0; 25 | 26 | -webkit-transform: rotate(0deg) translate(0px, -1px); 27 | -ms-transform: rotate(0deg) translate(0px, -1px); 28 | transform: rotate(0deg) translate(0px, -1px); 29 | } 30 | 31 | /* Remove these to get rid of the spinner */ 32 | #nprogress .spinner { 33 | display: block; 34 | position: fixed; 35 | z-index: 1031; 36 | top: 15px; 37 | right: 15px; 38 | } 39 | 40 | #nprogress .spinner-icon { 41 | width: 18px; 42 | height: 18px; 43 | box-sizing: border-box; 44 | 45 | border: solid 2px transparent; 46 | border-top-color: #29d; 47 | border-left-color: #29d; 48 | border-radius: 50%; 49 | 50 | -webkit-animation: nprogress-spinner 400ms linear infinite; 51 | animation: nprogress-spinner 400ms linear infinite; 52 | } 53 | 54 | .nprogress-custom-parent { 55 | overflow: hidden; 56 | position: relative; 57 | } 58 | 59 | .nprogress-custom-parent #nprogress .spinner, 60 | .nprogress-custom-parent #nprogress .bar { 61 | position: absolute; 62 | } 63 | 64 | @-webkit-keyframes nprogress-spinner { 65 | 0% { -webkit-transform: rotate(0deg); } 66 | 100% { -webkit-transform: rotate(360deg); } 67 | } 68 | @keyframes nprogress-spinner { 69 | 0% { transform: rotate(0deg); } 70 | 100% { transform: rotate(360deg); } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /web/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/favicon.ico -------------------------------------------------------------------------------- /web/static/img/downloader/115.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/downloader/115.jpg -------------------------------------------------------------------------------- /web/static/img/downloader/aria2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/downloader/aria2.png -------------------------------------------------------------------------------- /web/static/img/downloader/pikpak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/downloader/pikpak.png -------------------------------------------------------------------------------- /web/static/img/downloader/qbittorrent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/downloader/qbittorrent.png -------------------------------------------------------------------------------- /web/static/img/downloader/transmission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/downloader/transmission.png -------------------------------------------------------------------------------- /web/static/img/filetree/application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/application.png -------------------------------------------------------------------------------- /web/static/img/filetree/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/code.png -------------------------------------------------------------------------------- /web/static/img/filetree/css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/css.png -------------------------------------------------------------------------------- /web/static/img/filetree/db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/db.png -------------------------------------------------------------------------------- /web/static/img/filetree/directory-lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/directory-lock.png -------------------------------------------------------------------------------- /web/static/img/filetree/directory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/directory.png -------------------------------------------------------------------------------- /web/static/img/filetree/doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/doc.png -------------------------------------------------------------------------------- /web/static/img/filetree/file-lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/file-lock.png -------------------------------------------------------------------------------- /web/static/img/filetree/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/file.png -------------------------------------------------------------------------------- /web/static/img/filetree/film.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/film.png -------------------------------------------------------------------------------- /web/static/img/filetree/flash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/flash.png -------------------------------------------------------------------------------- /web/static/img/filetree/folder_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/folder_open.png -------------------------------------------------------------------------------- /web/static/img/filetree/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/html.png -------------------------------------------------------------------------------- /web/static/img/filetree/java.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/java.png -------------------------------------------------------------------------------- /web/static/img/filetree/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/linux.png -------------------------------------------------------------------------------- /web/static/img/filetree/music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/music.png -------------------------------------------------------------------------------- /web/static/img/filetree/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/pdf.png -------------------------------------------------------------------------------- /web/static/img/filetree/php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/php.png -------------------------------------------------------------------------------- /web/static/img/filetree/picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/picture.png -------------------------------------------------------------------------------- /web/static/img/filetree/ppt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/ppt.png -------------------------------------------------------------------------------- /web/static/img/filetree/psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/psd.png -------------------------------------------------------------------------------- /web/static/img/filetree/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/ruby.png -------------------------------------------------------------------------------- /web/static/img/filetree/script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/script.png -------------------------------------------------------------------------------- /web/static/img/filetree/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/spinner.gif -------------------------------------------------------------------------------- /web/static/img/filetree/txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/txt.png -------------------------------------------------------------------------------- /web/static/img/filetree/xls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/xls.png -------------------------------------------------------------------------------- /web/static/img/filetree/zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/filetree/zip.png -------------------------------------------------------------------------------- /web/static/img/icon-imdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/icon-imdb.png -------------------------------------------------------------------------------- /web/static/img/icons/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/icons/1024.png -------------------------------------------------------------------------------- /web/static/img/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/icons/128.png -------------------------------------------------------------------------------- /web/static/img/icons/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/icons/144.png -------------------------------------------------------------------------------- /web/static/img/icons/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/icons/152.png -------------------------------------------------------------------------------- /web/static/img/icons/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/icons/167.png -------------------------------------------------------------------------------- /web/static/img/icons/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/icons/172.png -------------------------------------------------------------------------------- /web/static/img/icons/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/icons/180.png -------------------------------------------------------------------------------- /web/static/img/icons/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/icons/196.png -------------------------------------------------------------------------------- /web/static/img/icons/196_ALT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/icons/196_ALT.png -------------------------------------------------------------------------------- /web/static/img/icons/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/icons/216.png -------------------------------------------------------------------------------- /web/static/img/icons/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/icons/256.png -------------------------------------------------------------------------------- /web/static/img/icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/icons/512.png -------------------------------------------------------------------------------- /web/static/img/icons/512_ALT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/icons/512_ALT.png -------------------------------------------------------------------------------- /web/static/img/indexer/indexer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/indexer/indexer.jpg -------------------------------------------------------------------------------- /web/static/img/indexer/indexer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/indexer/indexer.png -------------------------------------------------------------------------------- /web/static/img/indexer/jackett.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/indexer/jackett.png -------------------------------------------------------------------------------- /web/static/img/indexer/prowlarr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/indexer/prowlarr.png -------------------------------------------------------------------------------- /web/static/img/logo/logo-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/logo/logo-16x16.png -------------------------------------------------------------------------------- /web/static/img/logo/logo-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/logo/logo-32x32.png -------------------------------------------------------------------------------- /web/static/img/logo/logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/logo/logo-black.png -------------------------------------------------------------------------------- /web/static/img/logo/logo-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/logo/logo-blue.png -------------------------------------------------------------------------------- /web/static/img/logo/logo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/logo/logo-transparent.png -------------------------------------------------------------------------------- /web/static/img/logo/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/logo/logo-white.png -------------------------------------------------------------------------------- /web/static/img/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/logo/logo.png -------------------------------------------------------------------------------- /web/static/img/mediaserver/emby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/mediaserver/emby.png -------------------------------------------------------------------------------- /web/static/img/mediaserver/emby_backdrop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/mediaserver/emby_backdrop.png -------------------------------------------------------------------------------- /web/static/img/mediaserver/jellyfin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/mediaserver/jellyfin.jpg -------------------------------------------------------------------------------- /web/static/img/mediaserver/jellyfin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/mediaserver/jellyfin.png -------------------------------------------------------------------------------- /web/static/img/mediaserver/jellyfin_backdrop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/mediaserver/jellyfin_backdrop.jpg -------------------------------------------------------------------------------- /web/static/img/mediaserver/plex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/mediaserver/plex.png -------------------------------------------------------------------------------- /web/static/img/mediaserver/plex_backdrop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/mediaserver/plex_backdrop.png -------------------------------------------------------------------------------- /web/static/img/message/bark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/message/bark.webp -------------------------------------------------------------------------------- /web/static/img/message/chanify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/message/chanify.png -------------------------------------------------------------------------------- /web/static/img/message/gotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/message/gotify.png -------------------------------------------------------------------------------- /web/static/img/message/iyuu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/message/iyuu.png -------------------------------------------------------------------------------- /web/static/img/message/ntfy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/message/ntfy.webp -------------------------------------------------------------------------------- /web/static/img/message/pushdeer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/message/pushdeer.png -------------------------------------------------------------------------------- /web/static/img/message/pushplus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/message/pushplus.jpg -------------------------------------------------------------------------------- /web/static/img/message/serverchan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/message/serverchan.png -------------------------------------------------------------------------------- /web/static/img/message/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/message/slack.png -------------------------------------------------------------------------------- /web/static/img/message/synologychat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/message/synologychat.png -------------------------------------------------------------------------------- /web/static/img/message/telegram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/message/telegram.png -------------------------------------------------------------------------------- /web/static/img/message/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/message/wechat.png -------------------------------------------------------------------------------- /web/static/img/movie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/movie.jpg -------------------------------------------------------------------------------- /web/static/img/music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/music.png -------------------------------------------------------------------------------- /web/static/img/no-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/no-image.png -------------------------------------------------------------------------------- /web/static/img/no-image.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/static/img/person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/person.png -------------------------------------------------------------------------------- /web/static/img/plugins/SpeedLimiter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/SpeedLimiter.jpg -------------------------------------------------------------------------------- /web/static/img/plugins/autosubtitles.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/autosubtitles.jpeg -------------------------------------------------------------------------------- /web/static/img/plugins/backup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/backup.png -------------------------------------------------------------------------------- /web/static/img/plugins/chinesesubfinder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/chinesesubfinder.png -------------------------------------------------------------------------------- /web/static/img/plugins/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/cloud.png -------------------------------------------------------------------------------- /web/static/img/plugins/cloudflare.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/cloudflare.jpg -------------------------------------------------------------------------------- /web/static/img/plugins/diskusage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/diskusage.jpg -------------------------------------------------------------------------------- /web/static/img/plugins/douban.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/douban.png -------------------------------------------------------------------------------- /web/static/img/plugins/emby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/emby.png -------------------------------------------------------------------------------- /web/static/img/plugins/hosts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/hosts.png -------------------------------------------------------------------------------- /web/static/img/plugins/iyuu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/iyuu.png -------------------------------------------------------------------------------- /web/static/img/plugins/jackett.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/jackett.png -------------------------------------------------------------------------------- /web/static/img/plugins/like.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/like.jpg -------------------------------------------------------------------------------- /web/static/img/plugins/mediasyncdel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/mediasyncdel.png -------------------------------------------------------------------------------- /web/static/img/plugins/movie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/movie.jpg -------------------------------------------------------------------------------- /web/static/img/plugins/nfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/nfo.png -------------------------------------------------------------------------------- /web/static/img/plugins/opensubtitles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/opensubtitles.png -------------------------------------------------------------------------------- /web/static/img/plugins/prowlarr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/prowlarr.png -------------------------------------------------------------------------------- /web/static/img/plugins/random.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/random.png -------------------------------------------------------------------------------- /web/static/img/plugins/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/refresh.png -------------------------------------------------------------------------------- /web/static/img/plugins/regex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/regex.png -------------------------------------------------------------------------------- /web/static/img/plugins/scraper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/scraper.png -------------------------------------------------------------------------------- /web/static/img/plugins/signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/signin.png -------------------------------------------------------------------------------- /web/static/img/plugins/synctimer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/synctimer.png -------------------------------------------------------------------------------- /web/static/img/plugins/tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/tag.png -------------------------------------------------------------------------------- /web/static/img/plugins/teamwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/teamwork.png -------------------------------------------------------------------------------- /web/static/img/plugins/torrentremover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/torrentremover.png -------------------------------------------------------------------------------- /web/static/img/plugins/torrenttransfer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/torrenttransfer.jpg -------------------------------------------------------------------------------- /web/static/img/plugins/webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/plugins/webhook.png -------------------------------------------------------------------------------- /web/static/img/pt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/pt.jpg -------------------------------------------------------------------------------- /web/static/img/sites/1ptba.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/1ptba.ico -------------------------------------------------------------------------------- /web/static/img/sites/acgrip.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/acgrip.ico -------------------------------------------------------------------------------- /web/static/img/sites/audiences.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/audiences.ico -------------------------------------------------------------------------------- /web/static/img/sites/dmhy.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/dmhy.ico -------------------------------------------------------------------------------- /web/static/img/sites/eztv.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/eztv.ico -------------------------------------------------------------------------------- /web/static/img/sites/freefarm.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/freefarm.ico -------------------------------------------------------------------------------- /web/static/img/sites/hddolby.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/hddolby.ico -------------------------------------------------------------------------------- /web/static/img/sites/hdfans.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/hdfans.ico -------------------------------------------------------------------------------- /web/static/img/sites/hhclub.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/hhclub.ico -------------------------------------------------------------------------------- /web/static/img/sites/icc2022.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/icc2022.ico -------------------------------------------------------------------------------- /web/static/img/sites/iyuu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/iyuu.png -------------------------------------------------------------------------------- /web/static/img/sites/leaves.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/leaves.ico -------------------------------------------------------------------------------- /web/static/img/sites/lemonhd.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/lemonhd.ico -------------------------------------------------------------------------------- /web/static/img/sites/mikanani.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/mikanani.ico -------------------------------------------------------------------------------- /web/static/img/sites/nyaa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/nyaa.png -------------------------------------------------------------------------------- /web/static/img/sites/piggo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/piggo.ico -------------------------------------------------------------------------------- /web/static/img/sites/rarbg.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/rarbg.ico -------------------------------------------------------------------------------- /web/static/img/sites/sharkpt.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/sharkpt.ico -------------------------------------------------------------------------------- /web/static/img/sites/torrentgalaxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/torrentgalaxy.png -------------------------------------------------------------------------------- /web/static/img/sites/wintersakura.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/wintersakura.ico -------------------------------------------------------------------------------- /web/static/img/sites/zmpt.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/sites/zmpt.ico -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1125-2436.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-1125-2436.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1136-640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-1136-640.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1170-2532.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-1170-2532.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1242-2208.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-1242-2208.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1242-2688.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-1242-2688.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1284-2778.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-1284-2778.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1334-750.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-1334-750.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1536-2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-1536-2048.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1620-2160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-1620-2160.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1668-2224.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-1668-2224.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1668-2388.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-1668-2388.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-1792-828.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-1792-828.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2048-1536.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-2048-1536.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2048-2732.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-2048-2732.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2160-1620.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-2160-1620.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2208-1242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-2208-1242.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2224-1668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-2224-1668.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2388-1668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-2388-1668.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2436-1125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-2436-1125.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2532-1170.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-2532-1170.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2688-1242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-2688-1242.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2732-2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-2732-2048.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-2778-1284.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-2778-1284.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-640-1136.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-640-1136.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-750-1334.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-750-1334.png -------------------------------------------------------------------------------- /web/static/img/splash/apple-splash-828-1792.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/splash/apple-splash-828-1792.png -------------------------------------------------------------------------------- /web/static/img/startup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/startup.jpg -------------------------------------------------------------------------------- /web/static/img/tmdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/tmdb.png -------------------------------------------------------------------------------- /web/static/img/tmdb.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/tmdb.webp -------------------------------------------------------------------------------- /web/static/img/tv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/tv.png -------------------------------------------------------------------------------- /web/static/img/users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/users.png -------------------------------------------------------------------------------- /web/static/img/webhook_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsuyelin/nas-tools/a4c55b137ae5cf9423fd1bc2eb6e6344d539e00b/web/static/img/webhook_icon.png -------------------------------------------------------------------------------- /web/static/js/fullcalendar/locales/zh-cn.js: -------------------------------------------------------------------------------- 1 | FullCalendar.globalLocales.push(function () { 2 | 'use strict'; 3 | 4 | return { 5 | code: 'zh-cn', 6 | week: { 7 | // GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效 8 | dow: 1, // Monday is the first day of the week. 9 | doy: 4, // The week that contains Jan 4th is the first week of the year. 10 | }, 11 | buttonText: { 12 | prev: '<', 13 | next: '>', 14 | today: '今天', 15 | month: '月', 16 | week: '周', 17 | day: '日', 18 | list: '日程', 19 | }, 20 | weekText: '周', 21 | allDayText: '全天', 22 | moreLinkText: function (n) { 23 | return '另外 ' + n + ' 个' 24 | }, 25 | noEventsText: '没有事件显示', 26 | }; 27 | 28 | }()); 29 | -------------------------------------------------------------------------------- /web/static/js/tabler/demo-theme.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Tabler v1.0.0-beta16 (https://tabler.io) 3 | * @version 1.0.0-beta16 4 | * @link https://tabler.io 5 | * Copyright 2018-2022 The Tabler Authors 6 | * Copyright 2018-2022 codecalm.net Paweł Kuna 7 | * Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE) 8 | */ 9 | !function(e){"function"==typeof define&&define.amd?define(e):e()}((function(){"use strict";var e,t="tablerTheme",n=new Proxy(new URLSearchParams(window.location.search),{get:function(e,t){return e.get(t)}});if(n.theme)localStorage.setItem(t,n.theme),e=n.theme;else{var o=localStorage.getItem(t);e=o||"light"}document.body.classList.remove("theme-dark","theme-light"),document.body.classList.add("theme-".concat(e))})); -------------------------------------------------------------------------------- /web/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NAStool", 3 | "short_name": "NAStool", 4 | "start_url": "../", 5 | "icons": [ 6 | { 7 | "src": "./img/logo/logo-black.png", 8 | "sizes": "192x192", 9 | "type": "image/png", 10 | "purpose": "any" 11 | }, 12 | { 13 | "src": "./img/icons/196.png", 14 | "sizes": "192x192", 15 | "type": "image/png", 16 | "purpose": "maskable" 17 | }, 18 | { 19 | "src": "./img/logo/logo-black.png", 20 | "sizes": "512x512", 21 | "type": "image/png", 22 | "purpose": "any" 23 | }, 24 | { 25 | "src": "./img/icons/512.png", 26 | "sizes": "512x512", 27 | "type": "image/png", 28 | "purpose": "maskable" 29 | } 30 | ], 31 | "theme_color": "#000000", 32 | "background_color": "#000000", 33 | "display": "standalone" 34 | } -------------------------------------------------------------------------------- /web/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | {% import 'macro/svg.html' as SVG %} 3 | {% import 'macro/head.html' as HEAD %} 4 | 5 | 6 | 7 | {{ HEAD.meta_link() }} 8 | 404 - NAStool 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
404
18 |

出错啦!

19 |

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

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

出错啦!

19 |

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

22 | 28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /web/templates/discovery/mediainfo.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/templates/discovery/person.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /web/templates/discovery/ranking.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/templates/macro/oops.html: -------------------------------------------------------------------------------- 1 | 2 | {% macro nodatafound(title, text) %} 3 |
4 |
5 |
6 |
7 |
8 |

{{ title }}

9 |

10 | {{ text }} 11 |

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

{{ title }}

25 |

26 | {{ text }} 27 |

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

{{ title }}

41 |

42 | {{ text }} 43 |

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

57 | 正在加载 58 |

59 |
60 |
61 |
62 | {% endmacro %} 63 | -------------------------------------------------------------------------------- /web/templates/site/sitelist.html: -------------------------------------------------------------------------------- 1 | {% import 'macro/oops.html' as OOPS %} 2 |
3 | 4 | 13 |
14 | 15 | {% if Count > 0 %} 16 |
17 |
18 | 37 |
38 |
39 | {% else %} 40 | {{ OOPS.nodatafound('没有站点', '没有找到任何站点,请正确维护站点信息。') }} 41 | {% endif %} 42 | --------------------------------------------------------------------------------