diff --git a/db/db.go b/db/db.go index d82cc8b..2b35242 100644 --- a/db/db.go +++ b/db/db.go @@ -126,13 +126,6 @@ func (c *Client) AddMediaWatchlist(m *ent.Media, episodes []int) (*ent.Media, er m.StorageID = r.ID } } - - targetDir := fmt.Sprintf("%s %s (%v)", m.NameCn, m.NameEn, strings.Split(m.AirDate, "-")[0]) - if !utils.IsChineseChar(m.NameCn) { - log.Warnf("name cn is not chinese name: %v", m.NameCn) - targetDir = fmt.Sprintf("%s (%v)", m.NameEn, strings.Split(m.AirDate, "-")[0]) - } - r, err := c.ent.Media.Create(). SetTmdbID(m.TmdbID). SetStorageID(m.StorageID). @@ -143,7 +136,7 @@ func (c *Client) AddMediaWatchlist(m *ent.Media, episodes []int) (*ent.Media, er SetMediaType(m.MediaType). SetAirDate(m.AirDate). SetResolution(m.Resolution). - SetTargetDir(targetDir). + SetTargetDir(m.TargetDir). AddEpisodeIDs(episodes...). Save(context.TODO()) return r, err @@ -325,6 +318,13 @@ type WebdavSetting struct { } func (c *Client) AddStorage(st *StorageInfo) error { + if !strings.HasSuffix(st.Settings["tv_path"], "/") { + st.Settings["tv_path"] += "/" + } + if !strings.HasSuffix(st.Settings["movie_path"], "/") { + st.Settings["movie_path"] += "/" + } + data, err := json.Marshal(st.Settings) if err != nil { diff --git a/server/server.go b/server/server.go index 0f8ddc9..57b5285 100644 --- a/server/server.go +++ b/server/server.go @@ -76,6 +76,7 @@ func (s *Server) Serve() error { tv.GET("/record/:id", HttpHandler(s.GetMediaDetails)) tv.DELETE("/record/:id", HttpHandler(s.DeleteFromWatchlist)) tv.GET("/resolutions", HttpHandler(s.GetAvailableResolutions)) + tv.GET("/suggest/:tmdb_id", HttpHandler(s.SuggestedSeriesFolderName)) } indexer := api.Group("/indexer") { diff --git a/server/storage.go b/server/storage.go index 1af3144..91cd5e8 100644 --- a/server/storage.go +++ b/server/storage.go @@ -4,7 +4,9 @@ import ( "fmt" "polaris/db" "polaris/log" + "polaris/pkg/utils" "strconv" + "strings" "github.com/gin-gonic/gin" "github.com/pkg/errors" @@ -35,3 +37,36 @@ func (s *Server) DeleteStorage(c *gin.Context) (interface{}, error) { err = s.db.DeleteStorage(id) return nil, err } + +func (s *Server) SuggestedSeriesFolderName(c *gin.Context) (interface{}, error) { + ids := c.Param("tmdb_id") + id, err := strconv.Atoi(ids) + if err != nil { + return nil, fmt.Errorf("id is not int: %v", ids) + } + var name, originalName, year string + d, err := s.MustTMDB().GetTvDetails(id, s.language) + if err != nil { + d1, err := s.MustTMDB().GetMovieDetails(id, s.language) + if err != nil { + return nil, errors.Wrap(err, "get movie details") + } + name = d1.Title + originalName = d1.OriginalTitle + year = strings.Split(d1.ReleaseDate, "-")[0] + + } else { + name = d.Name + originalName = d.OriginalName + year = strings.Split(d.FirstAirDate, "-")[0] + } + name = fmt.Sprintf("%s %s", name, originalName) + + if !utils.IsChineseChar(name) { + name = originalName + } + if year != "" { + name = fmt.Sprintf("%s (%s)", name, year) + } + return gin.H{"name": name}, nil +} diff --git a/server/watchlist.go b/server/watchlist.go index 897d770..340336d 100644 --- a/server/watchlist.go +++ b/server/watchlist.go @@ -53,6 +53,7 @@ type addWatchlistIn struct { TmdbID int `json:"tmdb_id" binding:"required"` StorageID int `json:"storage_id" ` Resolution string `json:"resolution" binding:"required"` + Folder string `json:"folder" binding:"required"` } func (s *Server) AddTv2Watchlist(c *gin.Context) (interface{}, error) { @@ -109,6 +110,7 @@ func (s *Server) AddTv2Watchlist(c *gin.Context) (interface{}, error) { AirDate: detail.FirstAirDate, Resolution: string(in.Resolution), StorageID: in.StorageID, + TargetDir: in.Folder, }, epIds) if err != nil { return nil, errors.Wrap(err, "add to list") diff --git a/ui/lib/navdrawer.dart b/ui/lib/navdrawer.dart index ae1028b..9b86a98 100644 --- a/ui/lib/navdrawer.dart +++ b/ui/lib/navdrawer.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:ui/activity.dart'; -import 'package:ui/search.dart'; import 'package:ui/system_settings.dart'; import 'package:ui/welcome_page.dart'; diff --git a/ui/lib/providers/APIs.dart b/ui/lib/providers/APIs.dart index 8e933d2..966b3db 100644 --- a/ui/lib/providers/APIs.dart +++ b/ui/lib/providers/APIs.dart @@ -15,6 +15,7 @@ class APIs { static final watchlistMovieUrl = "$_baseUrl/api/v1/media/movie/watchlist"; static final availableMoviesUrl = "$_baseUrl/api/v1/media/movie/resources/"; static final seriesDetailUrl = "$_baseUrl/api/v1/media/record/"; + static final suggestedTvName = "$_baseUrl/api/v1/media/suggest/"; static final searchAndDownloadUrl = "$_baseUrl/api/v1/indexer/download"; static final allIndexersUrl = "$_baseUrl/api/v1/indexer/"; static final addIndexerUrl = "$_baseUrl/api/v1/indexer/add"; diff --git a/ui/lib/providers/welcome_data.dart b/ui/lib/providers/welcome_data.dart index 25ea13a..18edc7b 100644 --- a/ui/lib/providers/welcome_data.dart +++ b/ui/lib/providers/welcome_data.dart @@ -17,6 +17,18 @@ final tvWatchlistDataProvider = FutureProvider.autoDispose((ref) async { return favList; }); +final suggestNameDataProvider = FutureProvider.autoDispose.family( + (ref, int arg) async { + final dio = await APIs.getDio(); + var resp = await dio.get(APIs.suggestedTvName + arg.toString()); + var sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + return sp.data["name"] as String; + }, +); + final movieWatchlistDataProvider = FutureProvider.autoDispose((ref) async { final dio = await APIs.getDio(); var resp = await dio.get(APIs.watchlistMovieUrl); @@ -75,14 +87,15 @@ class SearchPageData state = newState; } - Future submit2Watchlist( - int tmdbId, int storageId, String resolution, String mediaType) async { + Future submit2Watchlist(int tmdbId, int storageId, String resolution, + String mediaType, String folder) async { final dio = await APIs.getDio(); if (mediaType == "tv") { var resp = await dio.post(APIs.watchlistTvUrl, data: { "tmdb_id": tmdbId, "storage_id": storageId, - "resolution": resolution + "resolution": resolution, + "folder": folder }); var sp = ServerResponse.fromJson(resp.data); if (sp.code != 0) { @@ -93,7 +106,8 @@ class SearchPageData var resp = await dio.post(APIs.watchlistMovieUrl, data: { "tmdb_id": tmdbId, "storage_id": storageId, - "resolution": resolution + "resolution": resolution, + "folder": folder }); var sp = ServerResponse.fromJson(resp.data); if (sp.code != 0) { @@ -116,9 +130,11 @@ class SearchResponse { page: json["page"], totalPage: json["total_page"], totalResults: json["total_results"], - results: json["results"] == null ? []: json["results"] - .map((v) => SearchResult.fromJson(v)) - .toList()); + results: json["results"] == null + ? [] + : (json["results"] as List) + .map((v) => SearchResult.fromJson(v)) + .toList()); } } diff --git a/ui/lib/search.dart b/ui/lib/search.dart index fb30e91..49db4e4 100644 --- a/ui/lib/search.dart +++ b/ui/lib/search.dart @@ -98,8 +98,8 @@ class _SearchPageState extends ConsumerState { var f = NotificationListener( onNotification: (ScrollNotification scrollInfo) { if (scrollInfo is ScrollEndNotification && - scrollInfo.metrics.axisDirection == AxisDirection.down && - scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent) { + scrollInfo.metrics.axisDirection == AxisDirection.down && + scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent) { ref.read(searchPageDataProvider(q).notifier).queryNextPage(); } return true; @@ -136,45 +136,84 @@ class _SearchPageState extends ConsumerState { String resSelected = "1080p"; int storageSelected = 0; var storage = ref.watch(storageSettingProvider); + var name = ref.watch(suggestNameDataProvider(item.id!)); + var pathController = TextEditingController(); return AlertDialog( title: Text('添加剧集: ${item.name}'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - DropdownMenu( - label: const Text("清晰度"), - initialSelection: resSelected, - dropdownMenuEntries: const [ - DropdownMenuEntry(value: "720p", label: "720p"), - DropdownMenuEntry(value: "1080p", label: "1080p"), - DropdownMenuEntry(value: "4k", label: "4k"), - ], - onSelected: (value) { - setState(() { - resSelected = value!; - }); - }, - ), - storage.when( - data: (v) { - return DropdownMenu( - label: const Text("存储位置"), - initialSelection: storageSelected, - dropdownMenuEntries: v - .map((s) => DropdownMenuEntry( - label: s.name!, value: s.id)) - .toList(), - onSelected: (value) { - setState(() { - storageSelected = value!; - }); - }, - ); + content: SizedBox( + width: 500, + height: 200, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DropdownMenu( + label: const Text("清晰度"), + initialSelection: resSelected, + dropdownMenuEntries: const [ + DropdownMenuEntry(value: "720p", label: "720p"), + DropdownMenuEntry(value: "1080p", label: "1080p"), + DropdownMenuEntry(value: "4k", label: "4k"), + ], + onSelected: (value) { + setState(() { + resSelected = value!; + }); }, - error: (err, trace) => Text("$err"), - loading: () => const MyProgressIndicator()), - ], + ), + + storage.when( + data: (v) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DropdownMenu( + label: const Text("存储位置"), + initialSelection: storageSelected, + dropdownMenuEntries: v + .map((s) => DropdownMenuEntry( + label: s.name!, value: s.id)) + .toList(), + onSelected: (value) { + setState(() { + storageSelected = value!; + }); + }, + ), + + name.when( + data: (s) { + + final path = item.mediaType == "tv" + ? v[storageSelected] + .settings!["tv_path"] + : v[storageSelected] + .settings!["movie_path"]; + pathController.text = s; + return SizedBox( + //width: 300, + child: TextField ( + + controller: pathController, + decoration: InputDecoration( + labelText: "存储路径", + prefix: Text(path) + ), + ), + ); + }, + error: (error, stackTrace) => Text("$error"), + loading: () => const MyProgressIndicator( + size: 20, + ), + ), + ], + ); + }, + error: (err, trace) => Text("$err"), + loading: () => const MyProgressIndicator()), + ], + ), ), actions: [ TextButton( @@ -195,8 +234,12 @@ class _SearchPageState extends ConsumerState { ref .read(searchPageDataProvider(widget.query ?? "") .notifier) - .submit2Watchlist(item.id!, storageSelected, - resSelected, item.mediaType!); + .submit2Watchlist( + item.id!, + storageSelected, + resSelected, + item.mediaType!, + pathController.text); Navigator.of(context).pop(); }, ),