mirror of
https://github.com/simon-ding/polaris.git
synced 2026-02-06 23:21:00 +08:00
feat: add name suggesting
This commit is contained in:
16
db/db.go
16
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 {
|
||||
|
||||
@@ -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")
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<void> submit2Watchlist(
|
||||
int tmdbId, int storageId, String resolution, String mediaType) async {
|
||||
Future<void> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,8 +98,8 @@ class _SearchPageState extends ConsumerState<SearchPage> {
|
||||
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<SearchPage> {
|
||||
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: <Widget>[
|
||||
TextButton(
|
||||
@@ -195,8 +234,12 @@ class _SearchPageState extends ConsumerState<SearchPage> {
|
||||
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();
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user