Skip to content

Commit 0030b83

Browse files
authored
Front: Base pages (#64)
2 parents e3cf2ec + f77b8fd commit 0030b83

31 files changed

+1062
-51
lines changed

.env.example

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ MATCHER_API_KEY=
1414
# Where can Blee find the video files
1515
DATA_DIR=
1616

17+
# Transcoder
18+
# You should set this to a path where blee can write large amount of data, this is used as a cache by the transcoder.
19+
# It will automatically be cleaned up on blee's startup/shutdown/runtime.
20+
CACHE_ROOT=/tmp/blee_cache
21+
# Hardware transcoding (equivalent of --profile docker compose option).
22+
COMPOSE_PROFILES=cpu # cpu (no hardware acceleration) or vaapi or qsv or nvidia
23+
# the preset used during transcode. faster means worst quality, you can probably use a slower preset with hwaccels
24+
# warning: using vaapi hwaccel disable presets (they are not supported).
25+
GOCODER_PRESET=fast
26+
1727

1828
# Internal Stuff, modify iff you know what you are doing
1929
CONFIG_DIR=/data

api/api/src/controllers/movies.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,16 @@ async fn get_movies(
139139

140140
/// Get a Movie's Chapters
141141
#[openapi(tag = "Movies")]
142-
#[get("/<uuid>/chapters")]
142+
#[get("/<uuid>/chapters?<pagination..>")]
143143
async fn get_movie_chapters(
144144
db: Database<'_>,
145+
pagination: Pagination,
145146
uuid: Uuid,
146-
) -> ApiResult<Vec<ChapterResponseWithThumbnail>> {
147-
services::chapter::find_by_movie(&uuid, db.into_inner())
147+
) -> ApiPageResult<ChapterResponseWithThumbnail> {
148+
services::chapter::find_by_movie(&uuid, &pagination, db.into_inner())
148149
.await
149-
.map_or_else(|e| Err(ApiError::from(e)), |v| Ok(Json(v)))
150+
.map(|items| Page::from(items))
151+
.map_or_else(|e| Err(ApiError::from(e)), |v| Ok(v))
150152
}
151153

152154
/// Upload a Movie's Thumbnail

api/api/src/services/chapter.rs

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
use crate::dto::chapter::{ChapterResponseWithThumbnail, NewChapter};
1+
use crate::dto::{
2+
chapter::{ChapterResponseWithThumbnail, NewChapter},
3+
page::Pagination,
4+
};
25
use entity::{chapter, image, sea_orm_active_enums::ChapterTypeEnum};
36
use rocket::serde::uuid::Uuid;
4-
use sea_orm::{ColumnTrait, ConnectionTrait, DbErr, EntityTrait, QueryFilter, QueryOrder, Set};
7+
use sea_orm::{
8+
ColumnTrait, ConnectionTrait, DbErr, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set,
9+
};
510

611
pub async fn create_many<'s, 'a, C>(
712
chapters: &Vec<NewChapter>,
@@ -38,12 +43,15 @@ where
3843

3944
pub async fn find_by_movie<'a, C>(
4045
movie_uuid: &Uuid,
46+
pagination: &Pagination,
4147
connection: &'a C,
4248
) -> Result<Vec<ChapterResponseWithThumbnail>, DbErr>
4349
where
4450
C: ConnectionTrait,
4551
{
4652
let movie_chapters: Vec<(chapter::Model, Option<image::Model>)> = chapter::Entity::find()
53+
.offset(pagination.skip)
54+
.limit(pagination.take)
4755
.find_also_related(image::Entity)
4856
.filter(chapter::Column::MovieId.eq(movie_uuid.clone()))
4957
.order_by_asc(chapter::Column::StartTime)

api/api/src/services/package.rs

+4
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ where
125125
package::Relation::Artist.def(),
126126
Alias::new("artist"),
127127
)
128+
.column_as(
129+
Expr::col((Alias::new("artist"), artist::Column::Name)),
130+
"artist_name",
131+
)
128132
.apply_if(filters.artist, |q, artist_uuid| {
129133
q.filter(package::Column::ArtistId.eq(artist_uuid))
130134
})

api/api/tests/movies.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,13 @@ mod test_movie {
8787
.dispatch();
8888
assert_eq!(chapter_response.status(), Status::Ok);
8989
let chapter_value = response_json_value(chapter_response);
90-
let chapters = chapter_value.as_array().unwrap();
90+
let chapters = chapter_value
91+
.as_object()
92+
.unwrap()
93+
.get("items")
94+
.unwrap()
95+
.as_array()
96+
.unwrap();
9197
assert_eq!(chapters.len(), 2);
9298
let chapter_one = chapters.first().unwrap();
9399
let chapter_name = chapter_one.get("name").unwrap().as_str().unwrap();

docker-compose.dev.yml

+55
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
x-transcoder: &transcoder-base
2+
image: ghcr.io/zoriya/kyoo_transcoder:4.5.0
3+
networks:
4+
default:
5+
aliases:
6+
- transcoder
7+
ports:
8+
- "7666:7666"
9+
restart: on-failure
10+
cpus: 1
11+
env_file:
12+
- ./.env
13+
environment:
14+
- GOCODER_PREFIX=/videos
15+
volumes:
16+
- ${DATA_DIR}:/videos:ro
17+
- ${CACHE_ROOT}:/cache
18+
- metadata:/metadata
19+
120
services:
221
scanner:
322
build:
@@ -81,7 +100,43 @@ services:
81100
retries: 10
82101
env_file:
83102
- .env
103+
transcoder:
104+
<<: *transcoder-base
105+
profiles: ['', 'cpu']
106+
107+
transcoder-nvidia:
108+
<<: *transcoder-base
109+
deploy:
110+
resources:
111+
reservations:
112+
devices:
113+
- capabilities: [gpu]
114+
environment:
115+
- GOCODER_PREFIX=/video
116+
- GOCODER_HWACCEL=nvidia
117+
profiles: ['nvidia']
118+
119+
transcoder-vaapi:
120+
<<: *transcoder-base
121+
devices:
122+
- /dev/dri:/dev/dri
123+
environment:
124+
- GOCODER_PREFIX=/video
125+
- GOCODER_HWACCEL=vaapi
126+
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
127+
profiles: ['vaapi']
128+
# qsv is the same setup as vaapi but with the hwaccel env var different
129+
transcoder-qsv:
130+
<<: *transcoder-base
131+
devices:
132+
- /dev/dri:/dev/dri
133+
environment:
134+
- GOCODER_PREFIX=/video
135+
- GOCODER_HWACCEL=qsv
136+
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
137+
profiles: ['qsv']
84138
volumes:
85139
db:
86140
api_target:
87141
api_cargo:
142+
metadata:

docker-compose.yml

+51
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
x-transcoder: &transcoder-base
2+
image: ghcr.io/zoriya/kyoo_transcoder:4.5.0
3+
networks:
4+
default:
5+
aliases:
6+
- transcoder
7+
restart: unless-stopped
8+
environment:
9+
- GOCODER_PREFIX=/videos
10+
volumes:
11+
- ${DATA_DIR}:/videos:ro
12+
- ${CACHE_ROOT}:/cache
13+
- metadata:/metadata
14+
115
# This compose allows to build the whole project in a "production"-like environment
216
services:
317
front:
@@ -100,8 +114,45 @@ services:
100114
- PORT=5000
101115
- FRONT_URL=http://front:3000
102116
- SERVER_URL=http://api:8000
117+
- TRANSCODER_URL=http://transcoder:7666
103118
volumes:
104119
- ./nginx.conf.template:/etc/nginx/templates/blee.conf.template:ro
120+
transcoder:
121+
<<: *transcoder-base
122+
profiles: ['', 'cpu']
123+
124+
transcoder-nvidia:
125+
<<: *transcoder-base
126+
deploy:
127+
resources:
128+
reservations:
129+
devices:
130+
- capabilities: [gpu]
131+
environment:
132+
- GOCODER_PREFIX=/video
133+
- GOCODER_HWACCEL=nvidia
134+
profiles: ['nvidia']
135+
136+
transcoder-vaapi:
137+
<<: *transcoder-base
138+
devices:
139+
- /dev/dri:/dev/dri
140+
environment:
141+
- GOCODER_PREFIX=/video
142+
- GOCODER_HWACCEL=vaapi
143+
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
144+
profiles: ['vaapi']
145+
# qsv is the same setup as vaapi but with the hwaccel env var different
146+
transcoder-qsv:
147+
<<: *transcoder-base
148+
devices:
149+
- /dev/dri:/dev/dri
150+
environment:
151+
- GOCODER_PREFIX=/video
152+
- GOCODER_HWACCEL=qsv
153+
- GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128}
154+
profiles: ['qsv']
105155
volumes:
106156
db:
107157
data:
158+
metadata:

front/fonts/rubik.ttf

348 KB
Binary file not shown.

front/lib/api/src/client.dart

+41-1
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,52 @@ class APIClient {
1818
}
1919
}
2020

21+
String buildImageUrl(String uuid) {
22+
final route = "/images/$uuid";
23+
return _host + (kDebugMode ? route : "api$route");
24+
}
25+
2126
Future<Artist> getArtist(String uuid) async {
2227
var responseBody = await _request(RequestType.get, '/artists/$uuid');
2328
return Artist.fromJson(responseBody);
2429
}
2530

26-
Future<Map<String, dynamic>> _request(RequestType type, String route,
31+
Future<Package> getPackage(String uuid) async {
32+
var responseBody = await _request(RequestType.get, '/packages/$uuid');
33+
return Package.fromJson(responseBody);
34+
}
35+
36+
Future<Page<Package>> getPackages(
37+
{PageQuery page = const PageQuery()}) async {
38+
var responseBody = await _request(
39+
RequestType.get, '/packages?take=${page.take}&skip=${page.skip}');
40+
return Page.fromJson(
41+
responseBody, (x) => Package.fromJson(x as Map<String, dynamic>));
42+
}
43+
44+
Future<Page<Movie>> getMovies(String packageUuid) async {
45+
var responseBody =
46+
await _request(RequestType.get, '/movies?package=$packageUuid');
47+
return Page.fromJson(
48+
responseBody, (x) => Movie.fromJson(x as Map<String, dynamic>));
49+
}
50+
51+
Future<Page<Chapter>> getChapters(String movieUuid, PageQuery query) async {
52+
var responseBody = await _request(RequestType.get,
53+
'/movies/$movieUuid/chapters?take=${query.take}&skip=${query.skip}');
54+
return Page.fromJson(
55+
responseBody, (x) => Chapter.fromJson(x as Map<String, dynamic>));
56+
}
57+
58+
Future<Page<Extra>> getExtras(
59+
{String? packageUuid, PageQuery page = const PageQuery()}) async {
60+
var responseBody = await _request(RequestType.get,
61+
'/extras?package=$packageUuid&take=${page.take}&skip=${page.skip}');
62+
return Page.fromJson(
63+
responseBody, (x) => Extra.fromJson(x as Map<String, dynamic>));
64+
}
65+
66+
Future<dynamic> _request(RequestType type, String route,
2767
{Map<String, dynamic>? body, Map<String, dynamic>? params}) async {
2868
body ??= {};
2969
params ?? {};

front/lib/api/src/models/extra.dart

+2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ class Extra with _$Extra {
1212
required Image? thumbnail,
1313
@JsonKey(name: "package_id") required String? packageId,
1414
@JsonKey(name: "artist_id") required String artistId,
15+
@JsonKey(name: "artist_name") required String artistName,
1516
@JsonKey(name: "file_id") required String fileId,
1617
required int? discIndex,
18+
required int duration,
1719
required int? trackIndex,
1820
required List<ExtraType> type,
1921
}) = _Extra;

front/lib/api/src/models/image.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class Image with _$Image {
88
const factory Image({
99
required String id,
1010
required String blurhash,
11-
@JsonKey(name: "aspect_ratio") required int aspectRatio,
11+
@JsonKey(name: "aspect_ratio") required double aspectRatio,
1212
required List<String> colors,
1313
}) = _Image;
1414

front/lib/api/src/models/package.dart

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class Package with _$Package {
1212
required String slug,
1313
@JsonKey(name: "release_year") required DateTime? releaseDate,
1414
@JsonKey(name: "artist_id") required String? artistId,
15+
@JsonKey(name: "artist_name") required String? artistName,
1516
required Image? poster}) = _Package;
1617

1718
factory Package.fromJson(Map<String, dynamic> json) =>

front/lib/api/src/models/page.dart

+6
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,9 @@ class Page<T> with _$Page<T> {
2525
Map<String, dynamic> json, T Function(Object? json) fromJsonT) =>
2626
_$PageFromJson(json, fromJsonT);
2727
}
28+
29+
class PageQuery {
30+
final int take;
31+
final int skip;
32+
const PageQuery({this.take = 20, this.skip = 0});
33+
}

front/lib/main.dart

+13-37
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
import 'package:blee/api/api.dart';
1+
import 'package:blee/ui/src/breakpoints.dart';
22
import 'package:flutter/material.dart';
33
import 'package:flutter_riverpod/flutter_riverpod.dart';
4-
import 'package:riverpod_annotation/riverpod_annotation.dart';
5-
import 'package:skeletonizer/skeletonizer.dart';
6-
7-
part 'main.g.dart';
8-
9-
@riverpod
10-
Future<Artist> activity(ActivityRef ref) async {
11-
return await APIClient().getArtist("the-corrs");
12-
}
4+
import 'package:blee/router.dart';
5+
import 'package:responsive_framework/responsive_framework.dart';
136

147
void main() {
8+
// usePathUrlStrategy();
159
runApp(const ProviderScope(
1610
child: MyApp(),
1711
));
@@ -22,35 +16,17 @@ class MyApp extends StatelessWidget {
2216

2317
@override
2418
Widget build(BuildContext context) {
25-
return MaterialApp(
26-
title: 'Flutter Demo',
19+
return MaterialApp.router(
20+
title: 'Blee',
2721
theme: ThemeData(
28-
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
29-
useMaterial3: true,
22+
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
23+
useMaterial3: true,
24+
fontFamily: 'Rubik'),
25+
builder: (context, child) => ResponsiveBreakpoints.builder(
26+
child: child!,
27+
breakpoints: Breakpoints.getList(),
3028
),
31-
home: const MyHomePage(),
32-
);
33-
}
34-
}
35-
36-
class MyHomePage extends StatelessWidget {
37-
const MyHomePage({super.key});
38-
39-
@override
40-
Widget build(BuildContext context) {
41-
return Consumer(
42-
builder: (context, ref, child) {
43-
final AsyncValue<Artist> data = ref.watch(activityProvider);
44-
45-
return Center(
46-
child: Skeletonizer(
47-
enabled: data.isLoading,
48-
child: switch (data) {
49-
AsyncData(:final value) => Text(value.name),
50-
AsyncError(:final error) => Text(error.toString()),
51-
_ => Container(),
52-
}));
53-
},
29+
routerConfig: router,
5430
);
5531
}
5632
}

0 commit comments

Comments
 (0)