diff --git a/server/resources.go b/server/resources.go index da513291..49e49679 100644 --- a/server/resources.go +++ b/server/resources.go @@ -175,6 +175,10 @@ func (s *Server) DownloadAll(c *gin.Context) (interface{}, error) { if err != nil { return nil, errors.Wrap(err, "convert") } + return s.downloadAllEpisodes(id) +} + +func (s *Server) downloadAllEpisodes(id int) (interface{}, error) { m, err := s.db.GetMedia(id) if err != nil { return nil, errors.Wrap(err, "get media") @@ -186,3 +190,27 @@ func (s *Server) DownloadAll(c *gin.Context) (interface{}, error) { return []string{name}, err } + +func (s *Server) DownloadAllTv(c *gin.Context) (interface{}, error) { + tvs := s.db.GetMediaWatchlist(media.MediaTypeTv) + var allNames []string + for _, tv := range tvs { + names, err := s.downloadAllEpisodes(tv.ID) + if err == nil { + allNames = append(allNames, names.([]string)...) + } + } + return allNames, nil +} + +func (s *Server) DownloadAllMovies(c *gin.Context) (interface{}, error) { + movies := s.db.GetMediaWatchlist(media.MediaTypeMovie) + var allNames []string + for _, mv := range movies { + names, err := s.downloadAllEpisodes(mv.ID) + if err == nil { + allNames = append(allNames, names.([]string)...) + } + } + return allNames, nil +} diff --git a/server/server.go b/server/server.go index dd5cbbbb..de04fdaa 100644 --- a/server/server.go +++ b/server/server.go @@ -96,6 +96,8 @@ func (s *Server) Serve() error { tv.GET("/suggest/tv/:tmdb_id", HttpHandler(s.SuggestedSeriesFolderName)) tv.GET("/suggest/movie/:tmdb_id", HttpHandler(s.SuggestedMovieFolderName)) tv.GET("/downloadall/:id", HttpHandler(s.DownloadAll)) + tv.GET("/download/tv", HttpHandler(s.DownloadAllTv)) + tv.GET("/download/movie", HttpHandler(s.DownloadAllMovies)) } indexer := api.Group("/indexer") { diff --git a/server/setting.go b/server/setting.go index e3ca4827..8ac73292 100644 --- a/server/setting.go +++ b/server/setting.go @@ -105,6 +105,7 @@ func (s *Server) SetSetting(c *gin.Context) (interface{}, error) { return nil, nil } + func (s *Server) GetSetting(c *gin.Context) (interface{}, error) { tmdb := s.db.GetSetting(db.SettingTmdbApiKey) downloadDir := s.db.GetSetting(db.SettingDownloadDir) diff --git a/ui/lib/providers/APIs.dart b/ui/lib/providers/APIs.dart index 8d18ae76..98579d0c 100644 --- a/ui/lib/providers/APIs.dart +++ b/ui/lib/providers/APIs.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -8,9 +10,12 @@ class APIs { static final _baseUrl = baseUrl(); static final searchUrl = "$_baseUrl/api/v1/media/search"; static final editMediaUrl = "$_baseUrl/api/v1/media/edit"; - static final downloadAllUrl = "$_baseUrl/api/v1/media/downloadall/"; + static final downloadAllEpisodesUrl = "$_baseUrl/api/v1/media/downloadall/"; + static final downloadAllTvUrl = "$_baseUrl/api/v1/media/download/tv"; + static final downloadAllMovieUrl = "$_baseUrl/api/v1/media/download/movie"; static final settingsUrl = "$_baseUrl/api/v1/setting/do"; static final settingsGeneralUrl = "$_baseUrl/api/v1/setting/general"; + //static final singleSettingUrl = "$_baseUrl/api/v1/setting/"; static final watchlistTvUrl = "$_baseUrl/api/v1/media/tv/watchlist"; static final watchlistMovieUrl = "$_baseUrl/api/v1/media/movie/watchlist"; static final availableTorrentsUrl = "$_baseUrl/api/v1/media/torrents/"; @@ -50,6 +55,9 @@ class APIs { static final cronJobUrl = "$_baseUrl/api/v1/setting/cron/trigger"; + static final tvParseUrl = "$_baseUrl/api/v1/setting/parse/tv"; + static final movieParseUrl = "$_baseUrl/api/v1/setting/parse/movie"; + static const tmdbApiKey = "tmdb_api_key"; static const downloadDirKey = "download_dir"; @@ -114,4 +122,50 @@ class APIs { throw sp.message; } } + + static Future> downloadAllTv() async { + var resp = await getDio().get(APIs.downloadAllTvUrl); + + var sp = ServerResponse.fromJson(resp.data); + + if (sp.code != 0) { + throw sp.message; + } + return sp.data as List; + } + + static Future> downloadAllMovies() async { + var resp = await getDio().get(APIs.downloadAllMovieUrl); + + var sp = ServerResponse.fromJson(resp.data); + + if (sp.code != 0) { + throw sp.message; + } + return sp.data as List; + } + + static Future parseTvName(String s) async { + var resp = await getDio().post(APIs.tvParseUrl, data: {"s": s}); + + var sp = ServerResponse.fromJson(resp.data); + + if (sp.code != 0) { + throw sp.message; + } + JsonEncoder encoder = new JsonEncoder.withIndent(' '); + return encoder.convert(sp.data); + } + + static Future parseMovieName(String s) async { + var resp = await getDio().post(APIs.movieParseUrl, data: {"s": s}); + + var sp = ServerResponse.fromJson(resp.data); + + if (sp.code != 0) { + throw sp.message; + } + JsonEncoder encoder = new JsonEncoder.withIndent(' '); + return encoder.convert(sp.data); + } } diff --git a/ui/lib/providers/series_details.dart b/ui/lib/providers/series_details.dart index 26610a0d..5a6b35ac 100644 --- a/ui/lib/providers/series_details.dart +++ b/ui/lib/providers/series_details.dart @@ -84,7 +84,7 @@ class SeriesDetailData Future downloadall() async { final dio = APIs.getDio(); - var resp = await dio.get(APIs.downloadAllUrl + id!); + var resp = await dio.get(APIs.downloadAllEpisodesUrl + id!); var sp = ServerResponse.fromJson(resp.data); if (sp.code != 0) { throw sp.message; diff --git a/ui/lib/providers/settings.dart b/ui/lib/providers/settings.dart index 05927623..88a92548 100644 --- a/ui/lib/providers/settings.dart +++ b/ui/lib/providers/settings.dart @@ -32,7 +32,7 @@ var prowlarrSettingDataProvider = class EditSettingData extends AutoDisposeAsyncNotifier { @override FutureOr build() async { - final dio = await APIs.getDio(); + final dio = APIs.getDio(); var resp = await dio.get(APIs.settingsGeneralUrl); var rrr = ServerResponse.fromJson(resp.data); diff --git a/ui/lib/welcome_page.dart b/ui/lib/welcome_page.dart index 2bb3a29c..1b40edfe 100644 --- a/ui/lib/welcome_page.dart +++ b/ui/lib/welcome_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:ui/movie_watchlist.dart'; @@ -6,6 +7,8 @@ import 'package:ui/providers/APIs.dart'; import 'package:ui/providers/welcome_data.dart'; import 'package:ui/tv_details.dart'; import 'package:ui/widgets/progress_indicator.dart'; +import 'package:ui/widgets/utils.dart'; +import 'package:ui/widgets/widgets.dart'; class WelcomePage extends ConsumerStatefulWidget { const WelcomePage({super.key}); @@ -20,7 +23,7 @@ class WelcomePage extends ConsumerStatefulWidget { class WelcomePageState extends ConsumerState { //WelcomePageState({super.key}); - + final _formKey = GlobalKey(); bool onlyShowUnfinished = false; @override @@ -50,66 +53,94 @@ class WelcomePageState extends ConsumerState { _ => const MyProgressIndicator(), }; }(), - Row( + getMoreButtonAndActions(uri) + ], + ); + } + + Widget getMoreButtonAndActions(String uri) { + return Row( + children: [ + Expanded(child: Container()), + Column( children: [ Expanded(child: Container()), - Column( - children: [ - Expanded(child: Container()), - Padding( - padding: EdgeInsets.all(20), - child: MenuAnchor( - style: MenuStyle( - //minimumSize: WidgetStatePropertyAll(Size(400, 300)), - - backgroundColor: WidgetStatePropertyAll(Theme.of(context) - .colorScheme - .inversePrimary - .withOpacity(0.9)), - ), - menuChildren: [ - MenuItemButton( - onPressed: null, - child: CheckboxListTile( - value: onlyShowUnfinished, - onChanged: (b) { - setState(() { - onlyShowUnfinished = b!; - }); - }, - title: const Text( - "未完成", - style: TextStyle(fontSize: 16), - softWrap: false, - ), - controlAffinity: ListTileControlAffinity.leading, - ), - ), - ], - builder: (context, controller, child) { - return Opacity( - opacity: 0.7, - child: FloatingActionButton( - onPressed: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - child: const Icon(Icons.more_horiz), - )); - }, - ), + Padding( + padding: EdgeInsets.all(20), + child: MenuAnchor( + style: MenuStyle( + alignment: Alignment.topLeft, + backgroundColor: WidgetStatePropertyAll(Theme.of(context) + .colorScheme + .inversePrimary + .withOpacity(0.7)), ), - ], - ) + menuChildren: [parseName(), onlyUnfinished(), refreshAll(uri)], + builder: (context, controller, child) { + return Opacity( + opacity: 0.7, + child: FloatingActionButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: const Icon(Icons.more_horiz), + )); + }, + ), + ), ], - ), + ) ], ); } + Widget onlyUnfinished() { + return CheckboxListTile( + value: onlyShowUnfinished, + onChanged: (b) { + setState(() { + onlyShowUnfinished = b!; + }); + }, + title: const Text( + "未完成", + style: TextStyle(fontSize: 16), + softWrap: false, + ), + controlAffinity: ListTileControlAffinity.leading, + ); + } + + Widget refreshAll(String uri) { + return LoadingListTile( + icon: Icons.refresh, + text: "全部更新", + onPressed: () async { + if (uri == WelcomePage.routeMoivie) { + await APIs.downloadAllMovies().then((v) { + showSnakeBar("开始下载电影:$v"); + }); + } else { + await APIs.downloadAllTv().then((v) { + showSnakeBar("开始下载剧集:$v"); + }); + } + }, + ); + } + + Widget parseName() { + return ListTile( + leading: Icon(Icons.calculate), + title: Text("测试解析"), + onTap: () => _showNameParsingDialog(), + ); + } + bool isSmallScreen(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; return screenWidth < 600; @@ -135,6 +166,71 @@ class WelcomePageState extends ConsumerState { return MediaCard(item: item); }); } + + Future _showNameParsingDialog() async { + final resultController = TextEditingController(); + return showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('测试名称解析'), + content: SizedBox( + width: 500, + height: 400, + child: FormBuilder( + key: _formKey, + initialValue: {"name": "", "type": "tv"}, + child: Column( + children: [ + FormBuilderTextField( + name: "name", + decoration: InputDecoration(labelText: "要解析的名字"), + ), + FormBuilderDropdown( + name: "type", + items: [ + DropdownMenuItem( + value: "tv", + child: const Text("电视剧"), + ), + DropdownMenuItem(value: "movie", child: const Text("电影")) + ], + ), + Center( + child: Padding( + padding: EdgeInsets.all(10), + child: LoadingTextButton( + onPressed: () async { + if (_formKey.currentState!.saveAndValidate()) { + final values = _formKey.currentState!.value; + //print(values); + if (values["type"] == "tv") { + var s = await APIs.parseTvName(values["name"]); + resultController.text = s; + } else { + var s = + await APIs.parseMovieName(values["name"]); + resultController.text = s; + } + } + return; + }, + label: Text("解析")), + ), + ), + TextField( + maxLines: 8, + controller: resultController, + ) + ], + ), + ), + ), + ); + }, + ); + } } class MediaCard extends StatelessWidget { diff --git a/ui/lib/widgets/widgets.dart b/ui/lib/widgets/widgets.dart index def82910..ab0a15fb 100644 --- a/ui/lib/widgets/widgets.dart +++ b/ui/lib/widgets/widgets.dart @@ -141,8 +141,58 @@ class _MySliderState extends State { } } +class LoadingListTile extends StatefulWidget { + const LoadingListTile( + {super.key, + required this.onPressed, + required this.icon, + required this.text}); + final Future Function() onPressed; + final IconData icon; + final String text; + + @override + State createState() { + return LoadingListTileState(); + } +} + +class LoadingListTileState extends State { + bool loading = false; + + @override + Widget build(BuildContext context) { + return ListTile( + onTap: loading + ? null + : () async { + setState(() => loading = true); + try { + await widget.onPressed(); + } catch (e) { + showSnakeBar("操作失败:$e"); + } finally { + setState(() => loading = false); + } + }, + title: Text(widget.text), + leading: loading + ? Container( + width: 24, + height: 24, + padding: const EdgeInsets.all(2.0), + child: const CircularProgressIndicator( + color: Colors.grey, + strokeWidth: 3, + ), + ) + : Icon(widget.icon)); + } +} + class LoadingIconButton extends StatefulWidget { - const LoadingIconButton({super.key, required this.onPressed, required this.icon, this.tooltip}); + const LoadingIconButton( + {super.key, required this.onPressed, required this.icon, this.tooltip}); final Future Function() onPressed; final IconData icon; final String? tooltip; @@ -159,7 +209,7 @@ class _LoadingIconButtonState extends State { @override Widget build(BuildContext context) { return IconButton( - tooltip: widget.tooltip, + tooltip: widget.tooltip, onPressed: loading ? null : () async { @@ -187,7 +237,8 @@ class _LoadingIconButtonState extends State { } class LoadingTextButton extends StatefulWidget { - const LoadingTextButton({super.key, required this.onPressed, required this.label}); + const LoadingTextButton( + {super.key, required this.onPressed, required this.label}); final Future Function() onPressed; final Widget label;