add movie download history

This commit is contained in:
Simon Ding
2024-07-23 19:01:24 +08:00
parent d2439480c8
commit 11f7b51eb5
6 changed files with 247 additions and 110 deletions

View File

@@ -485,3 +485,8 @@ func (c *Client) SetSeasonAllEpisodeStatus(mediaID, seasonNum int, status episod
func (c *Client) TmdbIdInWatchlist(tmdb_id int) bool { func (c *Client) TmdbIdInWatchlist(tmdb_id int) bool {
return c.ent.Media.Query().Where(media.TmdbID(tmdb_id)).CountX(context.TODO()) > 0 return c.ent.Media.Query().Where(media.TmdbID(tmdb_id)).CountX(context.TODO()) > 0
} }
func (c *Client) GetDownloadHistory(mediaID int) ([]*ent.History, error) {
return c.ent.History.Query().Where(history.MediaID(mediaID)).All(context.TODO())
}

View File

@@ -1,6 +1,7 @@
package server package server
import ( import (
"fmt"
"polaris/ent" "polaris/ent"
"polaris/ent/episode" "polaris/ent/episode"
"polaris/log" "polaris/log"
@@ -72,3 +73,15 @@ func (s *Server) RemoveActivity(c *gin.Context) (interface{}, error) {
log.Infof("history record successful deleted: %v", his.SourceTitle) log.Infof("history record successful deleted: %v", his.SourceTitle)
return nil, nil return nil, nil
} }
func (s *Server) GetMediaDownloadHistory(c *gin.Context) (interface{}, error) {
var ids = c.Param("id")
id, err := strconv.Atoi(ids)
if err != nil {
return nil, fmt.Errorf("id is not correct: %v", ids)
}
his, err := s.db.GetDownloadHistory(id)
if err != nil {
return nil, errors.Wrap(err, "db")
}
return his, nil
}

View File

@@ -62,6 +62,7 @@ func (s *Server) Serve() error {
{ {
activity.GET("/", HttpHandler(s.GetAllActivities)) activity.GET("/", HttpHandler(s.GetAllActivities))
activity.DELETE("/:id", HttpHandler(s.RemoveActivity)) activity.DELETE("/:id", HttpHandler(s.RemoveActivity))
activity.GET("/media/:id", HttpHandler(s.GetMediaDownloadHistory))
} }
tv := api.Group("/media") tv := api.Group("/media")

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:ui/providers/APIs.dart'; import 'package:ui/providers/APIs.dart';
import 'package:ui/providers/activity.dart';
import 'package:ui/providers/series_details.dart'; import 'package:ui/providers/series_details.dart';
import 'package:ui/providers/settings.dart'; import 'package:ui/providers/settings.dart';
import 'package:ui/providers/welcome_data.dart'; import 'package:ui/providers/welcome_data.dart';
@@ -30,7 +31,6 @@ class _MovieDetailsPageState extends ConsumerState<MovieDetailsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var seriesDetails = ref.watch(mediaDetailsProvider(widget.id)); var seriesDetails = ref.watch(mediaDetailsProvider(widget.id));
var torrents = ref.watch(movieTorrentsDataProvider(widget.id));
var storage = ref.watch(storageSettingProvider); var storage = ref.watch(storageSettingProvider);
return seriesDetails.when( return seriesDetails.when(
@@ -40,122 +40,100 @@ class _MovieDetailsPageState extends ConsumerState<MovieDetailsPage> {
Card( Card(
margin: const EdgeInsets.all(4), margin: const EdgeInsets.all(4),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: Padding( child: Container(
padding: const EdgeInsets.all(10), decoration: BoxDecoration(
child: Row( image: DecorationImage(
children: <Widget>[ fit: BoxFit.fitWidth,
Flexible( opacity: 0.5,
flex: 1, image: NetworkImage(
child: Padding( "${APIs.imagesUrl}/${details.id}/backdrop.jpg",
padding: const EdgeInsets.all(10), headers: APIs.authHeaders))),
child: Image.network( child: Padding(
"${APIs.imagesUrl}/${details.id}/poster.jpg", padding: const EdgeInsets.all(10),
fit: BoxFit.contain, child: Row(
headers: APIs.authHeaders, children: <Widget>[
Flexible(
flex: 1,
child: Padding(
padding: const EdgeInsets.all(10),
child: Image.network(
"${APIs.imagesUrl}/${details.id}/poster.jpg",
fit: BoxFit.contain,
headers: APIs.authHeaders,
),
),
), ),
), Expanded(
), flex: 6,
Expanded( child: Row(
flex: 6,
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text("${details.resolution}"), Row(
const SizedBox( children: [
width: 30, Text("${details.resolution}"),
const SizedBox(
width: 30,
),
storage.when(
data: (value) {
for (final s in value) {
if (s.id == details.storageId) {
return Text(
"${s.name}(${s.implementation})");
}
}
return const Text("未知存储");
},
error: (error, stackTrace) =>
Text("$error"),
loading: () =>
const MyProgressIndicator()),
],
),
const Divider(thickness: 1, height: 1),
Text(
"${details.name} (${details.airDate!.split("-")[0]})",
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold),
),
const Text(""),
Text(
details.overview!,
), ),
storage.when(
data: (value) {
for (final s in value) {
if (s.id == details.storageId) {
return Text(
"${s.name}(${s.implementation})");
}
}
return const Text("未知存储");
},
error: (error, stackTrace) =>
Text("$error"),
loading: () =>
const MyProgressIndicator()),
], ],
), )),
const Divider(thickness: 1, height: 1), Column(
Text( children: [
"${details.name} (${details.airDate!.split("-")[0]})", IconButton(
style: const TextStyle( onPressed: () {
fontSize: 20, ref
fontWeight: FontWeight.bold), .read(mediaDetailsProvider(
), widget.id)
const Text(""), .notifier)
Text( .delete()
details.overview!, .whenComplete(() => context
), .go(WelcomePage.routeMoivie))
.onError((error, trace) =>
Utils.showSnakeBar(
"删除失败:$error"));
},
icon: const Icon(Icons.delete))
],
)
], ],
)), ),
Column( ),
children: [ ],
IconButton(
onPressed: () {
ref
.read(mediaDetailsProvider(widget.id)
.notifier)
.delete()
.whenComplete(() => context
.go(WelcomePage.routeMoivie))
.onError((error, trace) =>
Utils.showSnakeBar(
"删除失败:$error"));
},
icon: const Icon(Icons.delete))
],
)
],
),
), ),
], )),
),
),
), ),
torrents.when( NestedTabBar(
data: (v) { id: widget.id,
return DataTable( )
columns: const [
DataColumn(label: Text("名称")),
DataColumn(label: Text("大小")),
DataColumn(label: Text("seeders")),
DataColumn(label: Text("peers")),
DataColumn(label: Text("操作"))
],
rows: List.generate(v.length, (i) {
final torrent = v[i];
return DataRow(cells: [
DataCell(Text("${torrent.name}")),
DataCell(Text("${torrent.size?.readableFileSize()}")),
DataCell(Text("${torrent.seeders}")),
DataCell(Text("${torrent.peers}")),
DataCell(IconButton(
icon: const Icon(Icons.download),
onPressed: () {
ref
.read(movieTorrentsDataProvider(widget.id)
.notifier)
.download(torrent.link!)
.whenComplete(() => Utils.showSnakeBar(
"开始下载:${torrent.name}")).onError((error, trace) => Utils.showSnakeBar("操作失败: $error"));
},
))
]);
}),
);
},
error: (error, trace) => Text("$error"),
loading: () => const MyProgressIndicator()),
], ],
); );
}, },
@@ -165,3 +143,125 @@ class _MovieDetailsPageState extends ConsumerState<MovieDetailsPage> {
loading: () => const MyProgressIndicator()); loading: () => const MyProgressIndicator());
} }
} }
class NestedTabBar extends ConsumerStatefulWidget {
final String id;
const NestedTabBar({super.key, required this.id});
@override
_NestedTabBarState createState() => _NestedTabBarState();
}
class _NestedTabBarState extends ConsumerState<NestedTabBar>
with TickerProviderStateMixin {
late TabController _nestedTabController;
@override
void initState() {
super.initState();
_nestedTabController = new TabController(length: 2, vsync: this);
}
@override
void dispose() {
super.dispose();
_nestedTabController.dispose();
}
int selectedTab = 0;
@override
Widget build(BuildContext context) {
var torrents = ref.watch(movieTorrentsDataProvider(widget.id));
var histories = ref.watch(mediaHistoryDataProvider(widget.id));
return Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TabBar(
controller: _nestedTabController,
isScrollable: true,
onTap: (value) {
setState(() {
selectedTab = value;
});
},
tabs: const <Widget>[
Tab(
text: "下载记录",
),
Tab(
text: "资源",
),
],
),
Builder(builder: (context) {
if (selectedTab == 0) {
return histories.when(
data: (v) {
if (v.isEmpty) {
return const Center(
child: Text("无下载记录"),
);
}
return DataTable(
columns: const [
DataColumn(label: Text("#"), numeric: true),
DataColumn(label: Text("名称")),
DataColumn(label: Text("下载时间")),
],
rows: List.generate(v.length, (i) {
final activity = v[i];
return DataRow(cells: [
DataCell(Text("${activity.id}")),
DataCell(Text("${activity.sourceTitle}")),
DataCell(Text("${activity.date!.toLocal()}")),
]);
}));
},
error: (error, trace) => Text("$error"),
loading: () => const MyProgressIndicator());
} else {
return torrents.when(
data: (v) {
return DataTable(
columns: const [
DataColumn(label: Text("名称")),
DataColumn(label: Text("大小")),
DataColumn(label: Text("seeders")),
DataColumn(label: Text("peers")),
DataColumn(label: Text("操作"))
],
rows: List.generate(v.length, (i) {
final torrent = v[i];
return DataRow(cells: [
DataCell(Text("${torrent.name}")),
DataCell(Text("${torrent.size?.readableFileSize()}")),
DataCell(Text("${torrent.seeders}")),
DataCell(Text("${torrent.peers}")),
DataCell(IconButton(
icon: const Icon(Icons.download),
onPressed: () {
ref
.read(movieTorrentsDataProvider(widget.id)
.notifier)
.download(torrent.link!)
.whenComplete(() =>
Utils.showSnakeBar("开始下载:${torrent.name}"))
.onError((error, trace) =>
Utils.showSnakeBar("操作失败: $error"));
},
))
]);
}),
);
},
error: (error, trace) => Text("$error"),
loading: () => const MyProgressIndicator());
}
})
],
);
}
}

View File

@@ -27,6 +27,7 @@ class APIs {
static final loginUrl = "$_baseUrl/api/login"; static final loginUrl = "$_baseUrl/api/login";
static final loginSettingUrl = "$_baseUrl/api/v1/setting/auth"; static final loginSettingUrl = "$_baseUrl/api/v1/setting/auth";
static final activityUrl = "$_baseUrl/api/v1/activity/"; static final activityUrl = "$_baseUrl/api/v1/activity/";
static final activityMediaUrl = "$_baseUrl/api/v1/activity/media/";
static final imagesUrl = "$_baseUrl/api/v1/img"; static final imagesUrl = "$_baseUrl/api/v1/img";
static final tmdbImgBaseUrl = "$_baseUrl/api/v1/posters"; static final tmdbImgBaseUrl = "$_baseUrl/api/v1/posters";

View File

@@ -8,10 +8,27 @@ var activitiesDataProvider =
AsyncNotifierProvider.autoDispose<ActivityData, List<Activity>>( AsyncNotifierProvider.autoDispose<ActivityData, List<Activity>>(
ActivityData.new); ActivityData.new);
var mediaHistoryDataProvider = FutureProvider.autoDispose.family(
(ref, arg) async {
final dio = await APIs.getDio();
var resp = await dio.get("${APIs.activityMediaUrl}$arg");
final sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
List<Activity> activities = List.empty(growable: true);
for (final a in sp.data as List) {
activities.add(Activity.fromJson(a));
}
return activities;
},
);
class ActivityData extends AutoDisposeAsyncNotifier<List<Activity>> { class ActivityData extends AutoDisposeAsyncNotifier<List<Activity>> {
@override @override
FutureOr<List<Activity>> build() async { FutureOr<List<Activity>> build() async {
Timer(const Duration(seconds: 5), ref.invalidateSelf);//Periodically Refresh Timer(
const Duration(seconds: 5), ref.invalidateSelf); //Periodically Refresh
final dio = await APIs.getDio(); final dio = await APIs.getDio();
var resp = await dio.get(APIs.activityUrl); var resp = await dio.get(APIs.activityUrl);