feat: add name suggesting

This commit is contained in:
Simon Ding
2024-07-19 16:59:12 +08:00
parent 1786e44933
commit 80d802fb4c
8 changed files with 152 additions and 55 deletions

View File

@@ -126,13 +126,6 @@ func (c *Client) AddMediaWatchlist(m *ent.Media, episodes []int) (*ent.Media, er
m.StorageID = r.ID 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(). r, err := c.ent.Media.Create().
SetTmdbID(m.TmdbID). SetTmdbID(m.TmdbID).
SetStorageID(m.StorageID). SetStorageID(m.StorageID).
@@ -143,7 +136,7 @@ func (c *Client) AddMediaWatchlist(m *ent.Media, episodes []int) (*ent.Media, er
SetMediaType(m.MediaType). SetMediaType(m.MediaType).
SetAirDate(m.AirDate). SetAirDate(m.AirDate).
SetResolution(m.Resolution). SetResolution(m.Resolution).
SetTargetDir(targetDir). SetTargetDir(m.TargetDir).
AddEpisodeIDs(episodes...). AddEpisodeIDs(episodes...).
Save(context.TODO()) Save(context.TODO())
return r, err return r, err
@@ -325,6 +318,13 @@ type WebdavSetting struct {
} }
func (c *Client) AddStorage(st *StorageInfo) error { 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) data, err := json.Marshal(st.Settings)
if err != nil { if err != nil {

View File

@@ -76,6 +76,7 @@ func (s *Server) Serve() error {
tv.GET("/record/:id", HttpHandler(s.GetMediaDetails)) tv.GET("/record/:id", HttpHandler(s.GetMediaDetails))
tv.DELETE("/record/:id", HttpHandler(s.DeleteFromWatchlist)) tv.DELETE("/record/:id", HttpHandler(s.DeleteFromWatchlist))
tv.GET("/resolutions", HttpHandler(s.GetAvailableResolutions)) tv.GET("/resolutions", HttpHandler(s.GetAvailableResolutions))
tv.GET("/suggest/:tmdb_id", HttpHandler(s.SuggestedSeriesFolderName))
} }
indexer := api.Group("/indexer") indexer := api.Group("/indexer")
{ {

View File

@@ -4,7 +4,9 @@ import (
"fmt" "fmt"
"polaris/db" "polaris/db"
"polaris/log" "polaris/log"
"polaris/pkg/utils"
"strconv" "strconv"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -35,3 +37,36 @@ func (s *Server) DeleteStorage(c *gin.Context) (interface{}, error) {
err = s.db.DeleteStorage(id) err = s.db.DeleteStorage(id)
return nil, err 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
}

View File

@@ -53,6 +53,7 @@ type addWatchlistIn struct {
TmdbID int `json:"tmdb_id" binding:"required"` TmdbID int `json:"tmdb_id" binding:"required"`
StorageID int `json:"storage_id" ` StorageID int `json:"storage_id" `
Resolution string `json:"resolution" binding:"required"` Resolution string `json:"resolution" binding:"required"`
Folder string `json:"folder" binding:"required"`
} }
func (s *Server) AddTv2Watchlist(c *gin.Context) (interface{}, error) { 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, AirDate: detail.FirstAirDate,
Resolution: string(in.Resolution), Resolution: string(in.Resolution),
StorageID: in.StorageID, StorageID: in.StorageID,
TargetDir: in.Folder,
}, epIds) }, epIds)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "add to list") return nil, errors.Wrap(err, "add to list")

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:ui/activity.dart'; import 'package:ui/activity.dart';
import 'package:ui/search.dart';
import 'package:ui/system_settings.dart'; import 'package:ui/system_settings.dart';
import 'package:ui/welcome_page.dart'; import 'package:ui/welcome_page.dart';

View File

@@ -15,6 +15,7 @@ class APIs {
static final watchlistMovieUrl = "$_baseUrl/api/v1/media/movie/watchlist"; static final watchlistMovieUrl = "$_baseUrl/api/v1/media/movie/watchlist";
static final availableMoviesUrl = "$_baseUrl/api/v1/media/movie/resources/"; static final availableMoviesUrl = "$_baseUrl/api/v1/media/movie/resources/";
static final seriesDetailUrl = "$_baseUrl/api/v1/media/record/"; 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 searchAndDownloadUrl = "$_baseUrl/api/v1/indexer/download";
static final allIndexersUrl = "$_baseUrl/api/v1/indexer/"; static final allIndexersUrl = "$_baseUrl/api/v1/indexer/";
static final addIndexerUrl = "$_baseUrl/api/v1/indexer/add"; static final addIndexerUrl = "$_baseUrl/api/v1/indexer/add";

View File

@@ -17,6 +17,18 @@ final tvWatchlistDataProvider = FutureProvider.autoDispose((ref) async {
return favList; 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 movieWatchlistDataProvider = FutureProvider.autoDispose((ref) async {
final dio = await APIs.getDio(); final dio = await APIs.getDio();
var resp = await dio.get(APIs.watchlistMovieUrl); var resp = await dio.get(APIs.watchlistMovieUrl);
@@ -75,14 +87,15 @@ class SearchPageData
state = newState; state = newState;
} }
Future<void> submit2Watchlist( Future<void> submit2Watchlist(int tmdbId, int storageId, String resolution,
int tmdbId, int storageId, String resolution, String mediaType) async { String mediaType, String folder) async {
final dio = await APIs.getDio(); final dio = await APIs.getDio();
if (mediaType == "tv") { if (mediaType == "tv") {
var resp = await dio.post(APIs.watchlistTvUrl, data: { var resp = await dio.post(APIs.watchlistTvUrl, data: {
"tmdb_id": tmdbId, "tmdb_id": tmdbId,
"storage_id": storageId, "storage_id": storageId,
"resolution": resolution "resolution": resolution,
"folder": folder
}); });
var sp = ServerResponse.fromJson(resp.data); var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) { if (sp.code != 0) {
@@ -93,7 +106,8 @@ class SearchPageData
var resp = await dio.post(APIs.watchlistMovieUrl, data: { var resp = await dio.post(APIs.watchlistMovieUrl, data: {
"tmdb_id": tmdbId, "tmdb_id": tmdbId,
"storage_id": storageId, "storage_id": storageId,
"resolution": resolution "resolution": resolution,
"folder": folder
}); });
var sp = ServerResponse.fromJson(resp.data); var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) { if (sp.code != 0) {
@@ -116,9 +130,11 @@ class SearchResponse {
page: json["page"], page: json["page"],
totalPage: json["total_page"], totalPage: json["total_page"],
totalResults: json["total_results"], totalResults: json["total_results"],
results: json["results"] == null ? []: json["results"] results: json["results"] == null
.map((v) => SearchResult.fromJson(v)) ? []
.toList()); : (json["results"] as List)
.map((v) => SearchResult.fromJson(v))
.toList());
} }
} }

View File

@@ -98,8 +98,8 @@ class _SearchPageState extends ConsumerState<SearchPage> {
var f = NotificationListener( var f = NotificationListener(
onNotification: (ScrollNotification scrollInfo) { onNotification: (ScrollNotification scrollInfo) {
if (scrollInfo is ScrollEndNotification && if (scrollInfo is ScrollEndNotification &&
scrollInfo.metrics.axisDirection == AxisDirection.down && scrollInfo.metrics.axisDirection == AxisDirection.down &&
scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent) { scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent) {
ref.read(searchPageDataProvider(q).notifier).queryNextPage(); ref.read(searchPageDataProvider(q).notifier).queryNextPage();
} }
return true; return true;
@@ -136,45 +136,84 @@ class _SearchPageState extends ConsumerState<SearchPage> {
String resSelected = "1080p"; String resSelected = "1080p";
int storageSelected = 0; int storageSelected = 0;
var storage = ref.watch(storageSettingProvider); var storage = ref.watch(storageSettingProvider);
var name = ref.watch(suggestNameDataProvider(item.id!));
var pathController = TextEditingController();
return AlertDialog( return AlertDialog(
title: Text('添加剧集: ${item.name}'), title: Text('添加剧集: ${item.name}'),
content: Column( content: SizedBox(
mainAxisSize: MainAxisSize.min, width: 500,
children: [ height: 200,
DropdownMenu( child: Column(
label: const Text("清晰度"), crossAxisAlignment: CrossAxisAlignment.start,
initialSelection: resSelected, children: [
dropdownMenuEntries: const [ DropdownMenu(
DropdownMenuEntry(value: "720p", label: "720p"), label: const Text("清晰度"),
DropdownMenuEntry(value: "1080p", label: "1080p"), initialSelection: resSelected,
DropdownMenuEntry(value: "4k", label: "4k"), dropdownMenuEntries: const [
], DropdownMenuEntry(value: "720p", label: "720p"),
onSelected: (value) { DropdownMenuEntry(value: "1080p", label: "1080p"),
setState(() { DropdownMenuEntry(value: "4k", label: "4k"),
resSelected = value!; ],
}); 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!;
});
},
);
}, },
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: <Widget>[ actions: <Widget>[
TextButton( TextButton(
@@ -195,8 +234,12 @@ class _SearchPageState extends ConsumerState<SearchPage> {
ref ref
.read(searchPageDataProvider(widget.query ?? "") .read(searchPageDataProvider(widget.query ?? "")
.notifier) .notifier)
.submit2Watchlist(item.id!, storageSelected, .submit2Watchlist(
resSelected, item.mediaType!); item.id!,
storageSelected,
resSelected,
item.mediaType!,
pathController.text);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),