Compare commits

...

9 Commits
v0.1 ... v0.2.0

Author SHA1 Message Date
Simon Ding
6826422c2b feat: option to change file hash when update to webdav 2024-07-23 15:18:42 +08:00
Simon Ding
8d2ce9752b feat: all in adaptive scafford & change seed color 2024-07-23 14:02:27 +08:00
Simon Ding
7e5feaf998 feat: adaptive ui & change colors 2024-07-23 13:42:42 +08:00
Simon Ding
e0bdd88706 feat: change name 2024-07-23 10:23:09 +08:00
Simon Ding
74d5bf54b9 chore: update readme 2024-07-22 17:55:50 +08:00
Simon Ding
03a3bf6d90 fix: entrypoint 2024-07-22 17:21:30 +08:00
Simon Ding
ee23b75390 update readme 2024-07-22 16:32:09 +08:00
Simon Ding
6e9b88b09b chore: rename 2024-07-22 15:56:52 +08:00
Simon Ding
93525ae883 chore: rename 2024-07-22 15:50:55 +08:00
14 changed files with 255 additions and 121 deletions

View File

@@ -1,4 +1,4 @@
name: Create and publish a Docker image
name: build docker image
on:
workflow_dispatch:

View File

@@ -1,4 +1,4 @@
name: Create and publish a Docker image
name: release docker image
on:
workflow_dispatch:
@@ -12,7 +12,7 @@ env:
jobs:
build-and-push-image:
build-and-release-image:
runs-on: ubuntu-latest
permissions:
contents: read

View File

@@ -32,4 +32,6 @@ RUN apt-get update && apt-get -y install ca-certificates
# 将上一个阶段publish文件夹下的所有文件复制进来
COPY --from=builder /app/polaris .
EXPOSE 8080
EXPOSE 8080
ENTRYPOINT ["./polaris"]

View File

@@ -23,15 +23,42 @@ Polaris 是一个电视剧和电影的追踪软件。配置好了之后,当剧
最简单部署 Polaris 的方式是使用 docker compose
```yaml
services:
polaris:
image: ghcr.io/simon-ding/polaris:latest
restart: always
volumes:
- ./config/polaris:/app/data #程序配置文件路径
- /downloads:/downloads #下载路径,需要和下载客户端配置一致
- /data:/data #数据存储路径
- /data:/data #媒体数据存储路径也可以启动自己配置webdav存储
ports:
- 8080:8080
jackett: #资源提供者,也可以不安装使用已有的
image: lscr.io/linuxserver/jackett:latest
environment:
- PUID=1000
- PGID=1000
- TZ=Asia/Shanghai
- AUTO_UPDATE=false
volumes:
- ./config/jackett:/config
ports:
- 9117:9117
restart: always
transmission: #下载客户端,也可以不安装使用已有的
image: lscr.io/linuxserver/transmission:latest
container_name: transmission
environment:
- PUID=1000
- PGID=1000
- TZ=Asia/Shanghai
volumes:
- ./config/transmission:/config
- /downloads:/downloads #此路径要与polaris下载路径保持一致
ports:
- 9091:9091
- 51413:51413
- 51413:51413/udp
```
拉起之后访问 http://< ip >:8080 的形式访问

View File

@@ -311,11 +311,12 @@ type LocalDirSetting struct {
}
type WebdavSetting struct {
URL string `json:"url"`
TvPath string `json:"tv_path"`
MoviePath string `json:"movie_path"`
User string `json:"user"`
Password string `json:"password"`
URL string `json:"url"`
TvPath string `json:"tv_path"`
MoviePath string `json:"movie_path"`
User string `json:"user"`
Password string `json:"password"`
ChangeFileHash string `json:"change_file_hash"`
}
func (c *Client) AddStorage(st *StorageInfo) error {

View File

@@ -7,6 +7,7 @@ import (
"path/filepath"
"polaris/log"
"polaris/pkg/gowebdav"
"polaris/pkg/utils"
"github.com/gabriel-vasile/mimetype"
"github.com/pkg/errors"
@@ -15,9 +16,10 @@ import (
type WebdavStorage struct {
fs *gowebdav.Client
dir string
changeMediaHash bool
}
func NewWebdavStorage(url, user, password, path string) (*WebdavStorage, error) {
func NewWebdavStorage(url, user, password, path string, changeMediaHash bool) (*WebdavStorage, error) {
c := gowebdav.NewClient(url, user, password)
if err := c.Connect(); err != nil {
return nil, errors.Wrap(err, "connect webdav")
@@ -53,6 +55,11 @@ func (w *WebdavStorage) Move(local, remote string) error {
// }
} else { //is file
if w.changeMediaHash {
if err := utils.ChangeFileHash(path); err != nil {
log.Errorf("change file %v hash error: %v", path, err)
}
}
if f, err := os.OpenFile(path, os.O_RDONLY, 0666); err != nil {
return errors.Wrapf(err, "read file %v", path)
} else { //open success

View File

@@ -1,6 +1,7 @@
package utils
import (
"os"
"regexp"
"strconv"
"strings"
@@ -146,3 +147,16 @@ func AvailableSpace(dir string) uint64 {
unix.Statfs(dir, &stat)
return stat.Bavail * uint64(stat.Bsize)
}
func ChangeFileHash(name string) error {
f, err := os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0655)
if err != nil {
return errors.Wrap(err, "open file")
}
defer f.Close()
_, err = f.Write([]byte("\000"))
if err != nil {
return errors.Wrap(err, "write file")
}
return nil
}

View File

@@ -101,7 +101,7 @@ func (s *Server) moveCompletedTask(id int) (err1 error) {
if series.MediaType == media.MediaTypeMovie {
targetPath = ws.MoviePath
}
storageImpl, err := storage.NewWebdavStorage(ws.URL, ws.User, ws.Password, targetPath)
storageImpl, err := storage.NewWebdavStorage(ws.URL, ws.User, ws.Password, targetPath, ws.ChangeFileHash == "true")
if err != nil {
return errors.Wrap(err, "new webdav")
}
@@ -162,7 +162,7 @@ func (s *Server) checkDownloadedSeriesFiles(m *ent.Media) error {
case storage1.ImplementationWebdav:
ws := st.ToWebDavSetting()
targetPath := ws.TvPath
storageImpl1, err := storage.NewWebdavStorage(ws.URL, ws.User, ws.Password, targetPath)
storageImpl1, err := storage.NewWebdavStorage(ws.URL, ws.User, ws.Password, targetPath, ws.ChangeFileHash == "true")
if err != nil {
return errors.Wrap(err, "new webdav")
}

View File

@@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:ui/activity.dart';
import 'package:ui/login_page.dart';
import 'package:ui/movie_watchlist.dart';
import 'package:ui/navdrawer.dart';
import 'package:ui/providers/APIs.dart';
import 'package:ui/search.dart';
import 'package:ui/system_settings.dart';
@@ -16,9 +16,18 @@ void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
class MyApp extends ConsumerStatefulWidget {
const MyApp({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() {
return _MyAppState();
}
}
class _MyAppState extends ConsumerState<MyApp> {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
@@ -26,95 +35,8 @@ class MyApp extends StatelessWidget {
final shellRoute = ShellRoute(
builder: (BuildContext context, GoRouterState state, Widget child) {
return SelectionArea(
child: Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: const Row(
children: [
Text("Polaris"),
],
),
actions: [
SearchAnchor(builder:
(BuildContext context, SearchController controller) {
return Container(
constraints:
const BoxConstraints(maxWidth: 300, maxHeight: 40),
child: Opacity(
opacity: 0.8,
child: SearchBar(
hintText: "搜索...",
leading: const Icon(Icons.search),
controller: controller,
shadowColor: WidgetStateColor.transparent,
backgroundColor: const WidgetStatePropertyAll(Color.fromARGB(255, 29, 78, 119)),
onSubmitted: (value) => context.go(Uri(
path: SearchPage.route,
queryParameters: {'query': value}).toString()),
),
),
);
}, suggestionsBuilder:
(BuildContext context, SearchController controller) {
return [Text("dadada")];
}),
FutureBuilder(
future: APIs.isLoggedIn(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data == true) {
return MenuAnchor(
menuChildren: [
MenuItemButton(
leadingIcon: const Icon(Icons.exit_to_app),
child: const Text("登出"),
onPressed: () async {
final SharedPreferences prefs =
await SharedPreferences.getInstance();
await prefs.remove('token');
if (context.mounted) {
context.go(LoginScreen.route);
}
},
),
],
builder: (context, controller, child) {
return TextButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Icon(Icons.account_circle),
);
},
);
}
return Container();
})
],
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Flex(direction: Axis.horizontal, children: <Widget>[
const Flexible(
flex: 1,
child: NavDrawer(),
),
const VerticalDivider(thickness: 1, width: 1),
Flexible(
flex: 7,
child:
Padding(padding: const EdgeInsets.all(20), child: child),
)
]))),
child: MainSkeleton(body: Padding(padding: const EdgeInsets.all(20), child: child),
),
);
},
routes: [
@@ -173,24 +95,159 @@ class MyApp extends StatelessWidget {
theme: ThemeData(
fontFamily: "NotoSansSC",
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue, brightness: Brightness.dark),
seedColor: Colors.blueAccent, brightness: Brightness.dark, surface: Colors.black54),
useMaterial3: true,
//scaffoldBackgroundColor: Color.fromARGB(255, 26, 24, 24)
),
routerConfig: router,
),
);
}
}
CustomTransitionPage buildPageWithDefaultTransition<T>({
required BuildContext context,
required GoRouterState state,
required Widget child,
}) {
return CustomTransitionPage<T>(
key: state.pageKey,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
);
class MainSkeleton extends StatefulWidget {
final Widget body;
const MainSkeleton({super.key, required this.body});
@override
State<StatefulWidget> createState() {
return _MainSkeletonState();
}
}
class _MainSkeletonState extends State<MainSkeleton> {
var _selectedTab;
@override
Widget build(BuildContext context) {
var uri = GoRouterState.of(context).uri.toString();
if (uri.contains(WelcomePage.routeTv)) {
_selectedTab = 0;
} else if (uri.contains(WelcomePage.routeMoivie)) {
_selectedTab = 1;
} else if (uri.contains(ActivityPage.route)) {
_selectedTab = 2;
} else if (uri.contains(SystemSettingsPage.route)) {
_selectedTab = 3;
}
return AdaptiveScaffold(
appBarBreakpoint: Breakpoints.standard,
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: const Row(
children: [
Text("Polaris"),
],
),
actions: [
SearchAnchor(builder:
(BuildContext context, SearchController controller) {
return Container(
constraints:
const BoxConstraints(maxWidth: 300, maxHeight: 40),
child: Opacity(
opacity: 0.8,
child: SearchBar(
hintText: "搜索...",
leading: const Icon(Icons.search),
controller: controller,
shadowColor: WidgetStateColor.transparent,
backgroundColor: WidgetStatePropertyAll(
Theme.of(context).colorScheme.primaryContainer
),
onSubmitted: (value) => context.go(Uri(
path: SearchPage.route,
queryParameters: {'query': value}).toString()),
),
),
);
}, suggestionsBuilder:
(BuildContext context, SearchController controller) {
return [Text("dadada")];
}),
FutureBuilder(
future: APIs.isLoggedIn(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data == true) {
return MenuAnchor(
menuChildren: [
MenuItemButton(
leadingIcon: const Icon(Icons.exit_to_app),
child: const Text("登出"),
onPressed: () async {
final SharedPreferences prefs =
await SharedPreferences.getInstance();
await prefs.remove('token');
if (context.mounted) {
context.go(LoginScreen.route);
}
},
),
],
builder: (context, controller, child) {
return TextButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Icon(Icons.account_circle),
);
},
);
}
return Container();
})
],
),
useDrawer: false,
selectedIndex: _selectedTab,
onSelectedIndexChange: (int index) {
setState(() {
_selectedTab = index;
});
if (index == 0) {
context.go(WelcomePage.routeTv);
} else if (index == 1) {
context.go(WelcomePage.routeMoivie);
} else if (index == 2) {
context.go(ActivityPage.route);
} else if (index == 3) {
context.go(SystemSettingsPage.route);
}
},
destinations: const <NavigationDestination>[
NavigationDestination(
icon: Icon(Icons.live_tv),
label: '电视剧',
),
NavigationDestination(
icon: Icon(Icons.movie),
label: '电影',
),
NavigationDestination(
icon: Icon(Icons.download),
label: '活动',
),
NavigationDestination(
icon: Icon(Icons.settings),
label: '设置',
),
],
body: (context) => widget.body,
// Define a default secondaryBody.
// Override the default secondaryBody during the smallBreakpoint to be
// empty. Must use AdaptiveScaffold.emptyBuilder to ensure it is properly
// overridden.
);
}
}

View File

@@ -258,7 +258,12 @@ class StorageSettingData extends AutoDisposeAsyncNotifier<List<Storage>> {
class Storage {
Storage(
{this.id, this.name, this.implementation, this.settings, this.isDefault});
{this.id,
this.name,
this.implementation,
this.settings,
this.isDefault,
});
final int? id;
final String? name;

View File

@@ -386,12 +386,15 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
var urlController = TextEditingController();
var userController = TextEditingController();
var passController = TextEditingController();
bool enablingChangeFileHash = false;
if (s.settings != null) {
tvPathController.text = s.settings!["tv_path"] ?? "";
moviePathController.text = s.settings!["movie_path"] ?? "";
urlController.text = s.settings!["url"] ?? "";
userController.text = s.settings!["user"] ?? "";
passController.text = s.settings!["password"] ?? "";
enablingChangeFileHash =
s.settings!["change_file_hash"] == "true" ? true : false;
}
String selectImpl = s.implementation == null ? "local" : s.implementation!;
@@ -419,7 +422,7 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
),
selectImpl != "local"
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
decoration: const InputDecoration(labelText: "Webdav地址"),
@@ -433,6 +436,14 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
decoration: const InputDecoration(labelText: "密码"),
controller: passController,
),
CheckboxListTile(
title: const Text("上传时更改文件哈希", style: TextStyle(fontSize: 14),),
value: enablingChangeFileHash,
onChanged: (v) {
setState(() {
enablingChangeFileHash = v??false;
});
}),
],
)
: Container(),
@@ -456,7 +467,8 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
"movie_path": moviePathController.text,
"url": urlController.text,
"user": userController.text,
"password": passController.text
"password": passController.text,
"change_file_hash": enablingChangeFileHash ? "true" : "false"
},
));
}

View File

@@ -110,6 +110,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_adaptive_scaffold:
dependency: "direct main"
description:
name: flutter_adaptive_scaffold
sha256: "56d4d81fe88ecffe8ae96b8d89a1ae793c0a85035bb9b74ff28f20eea0cdbdc2"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.11+1"
flutter_lints:
dependency: "direct dev"
description:

View File

@@ -43,6 +43,7 @@ dependencies:
shared_preferences: ^2.2.3
percent_indicator: ^4.2.3
intl: ^0.19.0
flutter_adaptive_scaffold: ^0.1.11+1
dev_dependencies:
flutter_test:

View File

@@ -1,6 +1,6 @@
{
"name": "ui",
"short_name": "ui",
"name": "Polaris",
"short_name": "Polaris",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",