mirror of
https://github.com/simon-ding/polaris.git
synced 2026-06-10 03:57:39 +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
|
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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user