Merge pull request #16 from Trivernis/develop

Develop
main v1.0.0-rc.3
Julius Riegel 2 years ago committed by GitHub
commit eb2062e410
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,8 @@
# compiled output
out
__pycache__
target
dist
# IDEs and editors
mediarepo-api/.idea
@ -36,4 +39,4 @@ mediarepo-daemon/*.folded
# api
mediarepo-api/.idea
mediarepo-api/target
mediarepo-api/target

@ -91,7 +91,7 @@ jobs:
python-version: '^3.7'
- name: Build
run: python build.py build --daemon --verbose
run: python scripts/build.py daemon --verbose --install-tooling
- name: Upload artifacts
if: ${{ !env.ACT }}
@ -150,7 +150,7 @@ jobs:
DEBIAN_FRONTEND=noninteractive sudo apt-get install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev -y
- name: Build project
run: python build.py build --ui --verbose
run: python scripts/build.py ui --verbose --install-tooling
- name: Upload artifacts
if: ${{ !env.ACT }}

@ -51,7 +51,7 @@ jobs:
python-version: '^3.7'
- name: Build Daemon
run: python build.py build --daemon --verbose
run: python scripts/build.py daemon --verbose --install-tooling
- uses: vimtor/action-zip@v1
with:
@ -113,7 +113,7 @@ jobs:
DEBIAN_FRONTEND=noninteractive sudo apt-get install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev -y
- name: Build project
run: python build.py build --ui --verbose
run: python scripts/build.py ui --verbose --install-tooling
- uses: vimtor/action-zip@v1
with:

1
.gitignore vendored

@ -4,6 +4,7 @@
/out-tsc
/target
/out
__pycache__
# IDEs and editors
mediarepo-api/.idea

@ -6,7 +6,7 @@ WORKDIR /usr/src
COPY mediarepo-api ./mediarepo-api
COPY mediarepo-daemon ./mediarepo-daemon
COPY mediarepo-ui ./mediarepo-ui
COPY build.py .
COPY scripts ./scripts
RUN apt-get update
RUN apt-get install -y \
@ -37,4 +37,6 @@ RUN apt remove cmdtest -y
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
RUN python3 build.py build
RUN python3 scripts/clean.py
RUN python3 scripts/check.py --install
RUN python3 scripts/build.py all --verbose --ffmpeg

@ -69,23 +69,32 @@ You also need to have a working [python](https://www.python.org/) installation o
After all required dependencies are installed and tools are accessible in the `PATH`, you can build the project like follows:
> Note: You might need to make the `build.py` file executable with `chmod +x build.py`.
Check (and install) required tooling:
```sh
$ ./scripts/check.py --install
```
> Note: this only installs tools that are installable via cargo or npm
All Componens:
```sh
$ ./build.py build --ffmpeg
$ ./scripts/build.py all --ffmpeg
```
Daemon only:
```sh
$ ./build.py build --daemon --ffmpeg
$ ./scripts/build.py daemon --ffmpeg
```
If you don't want to build with ffmpeg support omit the `--ffmpeg` flag.
UI only:
```sh
$ ./build.py build --ui
$ ./scripts/build.py ui
```
Clean the output directory:
```sh
$ ./scripts/clean.py
```
After building the `out` directory contains all the built binaries and bundles.

@ -1,210 +0,0 @@
#!/bin/env python3
import shutil as shut
import os
import subprocess
tauri_cli_version = '1.0.0-rc.5'
build_output = 'out'
verbose = False
ffmpeg = False
windows = os.name == 'nt'
def exec(cmd: str, dir: str = None) -> str:
print('Running: {}'.format(cmd))
child = subprocess.run(cmd, shell=True, cwd=dir)
child.check_returncode()
def check_exec(name: str):
print('Checking {}...'.format(name))
if shut.which(name) is None:
raise Exception('{} not found'.format(name))
exec(name + ' --version')
def check_yarn():
print('Checking yarn...')
if shut.which('yarn') is None:
print('installing yarn...')
npm('install -g yarn')
check_exec('yarn')
exec('yarn --version')
def check_ng():
print('Checking ng...')
if shut.which('ng') is None:
print('installing ng...')
npm('install -g @angular/cli')
check_exec('ng')
exec('ng --version')
def store_artifact(path: str):
print('Storing {}'.format(path))
if os.path.isdir(path):
shut.copytree(path, os.path.join(
build_output, os.path.basename(path)), dirs_exist_ok=True)
else:
shut.copy(path, build_output)
def cargo(cmd: str, dir: str = None):
if verbose:
exec('cargo {} --verbose'.format(cmd), dir)
else:
exec('cargo {}'.format(cmd), dir)
def npm(cmd: str, dir: str = None):
exec('npm {}'.format(cmd), dir)
def yarn(cmd: str, dir: str = None):
exec('yarn {}'.format(cmd), dir)
def build_daemon():
'''Builds daemon'''
cargo('fetch', 'mediarepo-daemon')
if not ffmpeg:
cargo('build --release --frozen --no-default-features', 'mediarepo-daemon')
else:
cargo('build --release --frozen', 'mediarepo-daemon')
if windows:
store_artifact('mediarepo-daemon/target/release/mediarepo-daemon.exe')
else:
store_artifact('mediarepo-daemon/target/release/mediarepo-daemon')
def build_ui():
'''Builds UI'''
cargo('install tauri-cli --version ^{}'.format(tauri_cli_version))
yarn('install', 'mediarepo-ui')
cargo('tauri build', 'mediarepo-ui')
if windows:
store_artifact(
'mediarepo-ui/src-tauri/target/release/mediarepo-ui.exe')
else:
store_artifact('mediarepo-ui/src-tauri/target/release/mediarepo-ui')
store_artifact('mediarepo-ui/src-tauri/target/release/bundle/')
def check_daemon():
'''Checks dependencies for daemon'''
check_exec('clang')
check_exec('cargo')
def check_ui():
'''Checks dependencies for UI'''
if not windows:
check_exec('wget')
check_exec('curl')
check_exec('file')
check_exec('clang')
check_exec('cargo')
check_exec('node')
check_exec('npm')
check_yarn()
check_ng()
def check():
'''Checks dependencies'''
check_daemon()
check_ui()
print('All checks passed')
def create_output_dir():
'''Creates build output directory'''
if not os.path.exists(build_output):
os.mkdir(build_output)
def clean():
'''Removes build output'''
if os.path.exists(build_output):
shut.rmtree(build_output)
print('Cleaned')
def build(daemon=True, ui=True):
'''Builds both daemon and UI'''
clean()
create_output_dir()
if daemon:
check_daemon()
build_daemon()
if ui:
check_ui()
build_ui()
print('Build complete')
def parse_args():
import argparse
parser = argparse.ArgumentParser(description='Build mediarepo')
subparsers = parser.add_subparsers(dest='command')
subparsers.required = True
subparsers.add_parser('check')
build_parser = subparsers.add_parser('build')
build_parser.add_argument(
'--daemon', action='store_true', help='Build daemon')
build_parser.add_argument('--ui', action='store_true', help='Build UI')
build_parser.add_argument(
'--verbose', action='store_true', help='Verbose build')
build_parser.add_argument(
'--output', action='store', help='Build output directory')
build_parser.add_argument(
'--ffmpeg', action='store_true', help='Build with ffmpeg')
subparsers.add_parser('clean')
args = parser.parse_args()
return args
def main():
opts = parse_args()
if opts.command == 'build':
global build_output
build_output = opts.output if opts.output else build_output
global verbose
verbose = opts.verbose
global ffmpeg
ffmpeg = opts.ffmpeg
if opts.daemon:
build(True, False)
elif opts.ui:
build(False, True)
else:
build()
elif opts.command == 'check':
check()
elif opts.command == 'clean':
clean()
if __name__ == '__main__':
main()

@ -1398,7 +1398,7 @@ dependencies = [
[[package]]
name = "mediarepo-daemon"
version = "1.0.0-rc.1"
version = "1.0.0-rc.3"
dependencies = [
"console-subscriber",
"glob",

@ -4,7 +4,7 @@ default-members = ["mediarepo-core", "mediarepo-database", "mediarepo-logic", "m
[package]
name = "mediarepo-daemon"
version = "1.0.0-rc.2"
version = "1.0.0-rc.3"
edition = "2018"
license = "gpl-3"
repository = "https://github.com/Trivernis/mediarepo-daemon"

@ -1,6 +1,6 @@
{
"name": "mediarepo-ui",
"version": "1.0.0-rc.2",
"version": "1.0.0-rc.3",
"scripts": {
"ng": "ng",
"start": "ng serve",

@ -40,7 +40,7 @@ checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0"
[[package]]
name = "app"
version = "1.0.0-rc.1"
version = "1.0.0-rc.3"
dependencies = [
"mediarepo-api",
"serde",

@ -1,6 +1,6 @@
[package]
name = "app"
version = "1.0.0-rc.2"
version = "1.0.0-rc.3"
description = "The UI for the mediarepo media management tool"
authors = ["you"]
license = ""

@ -1,7 +1,7 @@
<div id="content">
<mat-tab-group #tabGroup (selectedTabChange)="this.onTabSelectionChange($event)" animationDuration="0"
class="main-tab-group">
<mat-tab [label]="this.selectedRepository? 'RepositoryData' : 'Repositories'">
<mat-tab [label]="this.selectedRepository? 'Repository' : 'Repositories'">
<app-repositories-tab></app-repositories-tab>
</mat-tab>
<mat-tab *ngFor="let tab of tabs; trackBy: trackByTabId">

@ -39,7 +39,9 @@ import {MatToolbarModule} from "@angular/material/toolbar";
import {
RepositoryDetailsViewComponent
} from "./repositories-tab/repository-details-view/repository-details-view.component";
import { EmptyTabComponent } from './empty-tab/empty-tab.component';
import {EmptyTabComponent} from "./empty-tab/empty-tab.component";
import {RepositoryOverviewComponent} from "./repositories-tab/repository-overview/repository-overview.component";
import {AboutDialogComponent} from "./repositories-tab/repository-overview/about-dialog/about-dialog.component";
@NgModule({
@ -54,6 +56,8 @@ import { EmptyTabComponent } from './empty-tab/empty-tab.component';
DownloadDaemonDialogComponent,
RepositoryDetailsViewComponent,
EmptyTabComponent,
RepositoryOverviewComponent,
AboutDialogComponent,
],
exports: [
CoreComponent,

@ -1,18 +1,6 @@
<div *ngIf="!selectedRepository" class="repo-page-content">
<div class="add-repo-tools">
<button (click)="openAddRepositoryDialog()" color="primary" mat-flat-button>Add Repository</button>
</div>
<div class="repository-list">
<div *ngFor="let repository of repositories" class="repository-container">
<app-repository-card (openEvent)="this.onOpenRepository($event)"
[repository]="repository"></app-repository-card>
</div>
<app-middle-centered *ngIf="this.repositories.length === 0" class="add-repository-prompt">
<h1>There are no repositories yet. You can create a repository or add an existing one.</h1>
<button (click)="this.openAddRepositoryDialog()" color="primary" mat-flat-button>Add Repository</button>
</app-middle-centered>
</div>
<div *ngIf="!this.selectedRepository">
<app-repository-overview></app-repository-overview>
</div>
<div *ngIf="selectedRepository" class="repo-details">
<app-repository-details-view [repository]="selectedRepository"></app-repository-details-view>
<div *ngIf="this.selectedRepository" class="repo-details">
<app-repository-details-view [repository]="this.selectedRepository"></app-repository-details-view>
</div>

@ -1,41 +1,3 @@
.repository-container {
margin: 1em;
}
.repo-page-content {
margin: 0 10%;
height: calc(100% - 2em);
}
.add-repo-tools {
height: 5em;
display: flex;
flex-direction: row-reverse;
button {
margin: 1em;
}
}
.repository-list {
display: flex;
flex-direction: column;
overflow-y: auto;
height: calc(100% - 5em);
}
app-repository-card {
position: relative;
}
app-repository-details-view, .repo-details {
app-repository-details-view {
height: 100%;
}
.add-repository-prompt {
button {
font-size: 1.5em;
padding: 0.5em 1em;
border-radius: 0.5em;
}
}

@ -1,164 +1,18 @@
import {AfterViewInit, Component, OnInit} from "@angular/core";
import {Repository} from "../../../../api/models/Repository";
import {Component} from "@angular/core";
import {RepositoryService} from "../../../services/repository/repository.service";
import {MatDialog, MatDialogRef} from "@angular/material/dialog";
import {DownloadDaemonDialogComponent} from "./download-daemon-dialog/download-daemon-dialog.component";
import {
AddRepositoryDialogComponent
} from "../../shared/repository/repository/add-repository-dialog/add-repository-dialog.component";
import {LoggingService} from "../../../services/logging/logging.service";
import {BehaviorSubject} from "rxjs";
import {BusyDialogComponent} from "../../shared/app-common/busy-dialog/busy-dialog.component";
import {JobService} from "../../../services/job/job.service";
import {StateService} from "../../../services/state/state.service";
import {Repository} from "../../../../api/models/Repository";
type BusyDialogContext = { message: BehaviorSubject<string>, dialog: MatDialogRef<BusyDialogComponent> };
@Component({
selector: "app-repositories-tab",
templateUrl: "./repositories-tab.component.html",
styleUrls: ["./repositories-tab.component.scss"]
})
export class RepositoriesTabComponent implements OnInit, AfterViewInit {
public repositories: Repository[] = [];
public selectedRepository?: Repository;
constructor(
private logger: LoggingService,
private repoService: RepositoryService,
private jobService: JobService,
private stateService: StateService,
public dialog: MatDialog
) {
}
ngOnInit(): void {
this.repoService.repositories.subscribe({
next: (repos) => {
this.repositories = repos;
}
});
this.repoService.selectedRepository.subscribe(
repo => this.selectedRepository = repo);
}
export class RepositoriesTabComponent {
public async ngAfterViewInit() {
await this.checkAndPromptDaemonExecutable();
}
public async startDaemonAndSelectRepository(repository: Repository) {
try {
let dialogContext = this.openStartupDialog(repository);
let daemonRunning = await this.repoService.checkDaemonRunning(
repository.path!);
if (!daemonRunning) {
dialogContext.message.next("Starting repository daemon...");
await this.repoService.startDaemon(repository.path!);
await new Promise((res, _) => {
setTimeout(res, 2000); // wait for the daemon to start
});
}
await this.selectRepository(repository, dialogContext);
} catch (err: any) {
this.logger.error(err);
}
}
public async selectRepository(repository: Repository, dialogContext?: BusyDialogContext) {
dialogContext = dialogContext ?? this.openStartupDialog(repository);
try {
dialogContext.message.next("Opening repository...");
await this.repoService.setRepository(repository);
await this.runRepositoryStartupTasks(dialogContext);
dialogContext.message.next("Restoring previous tabs...");
await this.repoService.loadRepositories();
dialogContext.dialog.close(true);
} catch (err: any) {
this.logger.error(err);
dialogContext.message.next(
"Failed to open repository: " + err.toString());
await this.forceCloseRepository();
setTimeout(() => dialogContext!.dialog.close(true), 1000);
}
}
public openAddRepositoryDialog() {
this.dialog.open(AddRepositoryDialogComponent, {
disableClose: true,
minWidth: "30%",
minHeight: "30%",
});
}
public async onOpenRepository(repository: Repository) {
if (!repository.local) {
await this.selectRepository(repository);
} else {
await this.startDaemonAndSelectRepository(repository);
}
}
private async forceCloseRepository() {
try {
await this.repoService.closeSelectedRepository();
} catch {
}
try {
await this.repoService.disconnectSelectedRepository();
} catch {
}
}
private async runRepositoryStartupTasks(dialogContext: BusyDialogContext): Promise<void> {
dialogContext.message.next("Checking integrity...");
await this.jobService.runJob("CheckIntegrity");
dialogContext.message.next("Running a vacuum on the database...");
await this.jobService.runJob("Vacuum");
dialogContext.message.next(
"Migrating content descriptors to new format...");
await this.jobService.runJob("MigrateContentDescriptors");
dialogContext.message.next("Calculating repository sizes...");
await this.jobService.runJob("CalculateSizes", false);
dialogContext.message.next("Generating missing thumbnails...");
await this.jobService.runJob("GenerateThumbnails");
dialogContext.message.next("Finished repository startup");
}
private openStartupDialog(repository: Repository): BusyDialogContext {
const dialogMessage = new BehaviorSubject<string>(
"Opening repository...");
let dialog = this.dialog.open(BusyDialogComponent, {
data: {
title: `Opening repository '${repository.name}'`,
message: dialogMessage,
allowCancel: true,
}, disableClose: true,
minWidth: "30%",
minHeight: "30%",
});
dialog.afterClosed().subscribe(async (result) => {
if (!result) {
await this.forceCloseRepository();
}
});
return { message: dialogMessage, dialog };
}
public selectedRepository?: Repository;
private async checkAndPromptDaemonExecutable() {
if (!await this.repoService.checkDameonConfigured()) {
const result = await this.dialog.open(
DownloadDaemonDialogComponent,
{
disableClose: true,
}
).afterClosed().toPromise();
if (result) {
// recursion avoidance
setTimeout(
async () => await this.checkAndPromptDaemonExecutable(), 0);
}
}
constructor(private repositoryService: RepositoryService) {
const sub = this.repositoryService.selectedRepository.subscribe(repo => this.selectedRepository = repo);
}
}

@ -0,0 +1,24 @@
<h1 class="title" mat-dialog-title>
About mediarepo
</h1>
<div class="content" mat-dialog-content>
<app-metadata-entry attributeName="Version" display="line">
{{version | async}}
</app-metadata-entry>
<app-metadata-entry attributeName="Tauri Version" display="line">
{{tauriVersion | async}}
</app-metadata-entry>
<br>
<mat-divider></mat-divider>
<h2>Built with</h2>
<div class="lib-list">
<b *ngFor="let lib of usedLibs">
<app-external-url [href]="lib[1]">{{lib[0]}}</app-external-url>
</b>
</div>
</div>
<div class="actions" mat-dialog-actions>
<button (click)="this.dialogRef.close(true)" color="primary" mat-stroked-button>
Close
</button>
</div>

@ -0,0 +1,20 @@
.title {
text-align: center;
}
.actions {
display: block;
button {
float: right;
}
}
.content {
text-align: center;
}
.lib-list {
display: flex;
flex-direction: column;
}

@ -0,0 +1,25 @@
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {AboutDialogComponent} from "./about-dialog.component";
describe("AboutDialogComponent", () => {
let component: AboutDialogComponent;
let fixture: ComponentFixture<AboutDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AboutDialogComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AboutDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,31 @@
import {ChangeDetectionStrategy, Component, OnInit} from "@angular/core";
import {MatDialogRef} from "@angular/material/dialog";
import {app} from "@tauri-apps/api";
import {Subject} from "rxjs";
@Component({
selector: "app-about-dialog",
templateUrl: "./about-dialog.component.html",
styleUrls: ["./about-dialog.component.scss"],
changeDetection: ChangeDetectionStrategy.Default
})
export class AboutDialogComponent implements OnInit {
public version = new Subject<string>();
public tauriVersion = new Subject<string>();
public usedLibs = [
["Tauri", "https://tauri.studio"],
["Angular", "https://angular.io/"],
["SeaORM", "https://www.sea-ql.org"],
["Tokio", "https://tokio.rs/"],
["bromine", "https://github.com/Trivernis/bromine"]
];
constructor(public dialogRef: MatDialogRef<AboutDialogComponent>) {
}
public async ngOnInit() {
this.version.next(await app.getVersion());
this.tauriVersion.next(await app.getTauriVersion());
}
}

@ -0,0 +1,18 @@
<mat-toolbar>
<h1 class="page-title">All Repositories</h1>
<button (click)="openAddRepositoryDialog()" class="add-repo-button" color="primary" mat-flat-button>Add Repository
</button>
<button (click)="openAboutDialog()" class="about-button" mat-stroked-button>About</button>
</mat-toolbar>
<div class="repo-page-content">
<div class="repository-list">
<div *ngFor="let repository of repositories" class="repository-container">
<app-repository-card (openEvent)="this.onOpenRepository($event)"
[repository]="repository"></app-repository-card>
</div>
<app-middle-centered *ngIf="this.repositories.length === 0" class="add-repository-prompt">
<h1>There are no repositories yet. You can create a repository or add an existing one.</h1>
<button (click)="this.openAddRepositoryDialog()" color="primary" mat-flat-button>Add Repository</button>
</app-middle-centered>
</div>
</div>

@ -0,0 +1,63 @@
.repository-container {
padding: 1em;
margin: auto;
display: block;
width: calc(600px - 2em);
float: left;
app-repository-card {
display: block;
position: relative;
}
}
.repo-page-content {
margin: 0 10%;
height: calc(100% - 2em);
}
.add-repo-tools {
height: 5em;
display: flex;
flex-direction: row-reverse;
button {
margin: 1em;
}
}
.repository-list {
display: flex;
flex-wrap: wrap;
overflow-y: auto;
overflow-x: hidden;
height: calc(100% - 5em);
}
.add-repository-prompt {
button {
font-size: 1.5em;
padding: 0.5em 1em;
border-radius: 0.5em;
}
}
mat-toolbar {
position: relative;
overflow: hidden;
.add-repo-button {
margin: 0 auto 0 0;
}
.about-button {
margin: 0 0 0 auto;
}
.page-title {
position: absolute;
width: calc(100% - 32px);
text-align: center;
}
}

@ -0,0 +1,25 @@
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {RepositoryOverviewComponent} from "./repository-overview.component";
describe("RepositoryOverviewComponent", () => {
let component: RepositoryOverviewComponent;
let fixture: ComponentFixture<RepositoryOverviewComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RepositoryOverviewComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(RepositoryOverviewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,167 @@
import {AfterViewInit, ChangeDetectionStrategy, Component, OnInit} from "@angular/core";
import {Repository} from "../../../../../api/models/Repository";
import {LoggingService} from "../../../../services/logging/logging.service";
import {RepositoryService} from "../../../../services/repository/repository.service";
import {JobService} from "../../../../services/job/job.service";
import {StateService} from "../../../../services/state/state.service";
import {MatDialog, MatDialogRef} from "@angular/material/dialog";
import {
AddRepositoryDialogComponent
} from "../../../shared/repository/repository/add-repository-dialog/add-repository-dialog.component";
import {BehaviorSubject} from "rxjs";
import {BusyDialogComponent} from "../../../shared/app-common/busy-dialog/busy-dialog.component";
import {DownloadDaemonDialogComponent} from "../download-daemon-dialog/download-daemon-dialog.component";
import {AboutDialogComponent} from "./about-dialog/about-dialog.component";
type BusyDialogContext = { message: BehaviorSubject<string>, dialog: MatDialogRef<BusyDialogComponent> };
@Component({
selector: "app-repository-overview",
templateUrl: "./repository-overview.component.html",
styleUrls: ["./repository-overview.component.scss"],
changeDetection: ChangeDetectionStrategy.Default
})
export class RepositoryOverviewComponent implements OnInit, AfterViewInit {
public repositories: Repository[] = [];
constructor(
private logger: LoggingService,
private repoService: RepositoryService,
private jobService: JobService,
private stateService: StateService,
public dialog: MatDialog
) {
}
ngOnInit(): void {
this.repoService.repositories.subscribe(repos => this.repositories = repos);
}
public async ngAfterViewInit() {
await this.checkAndPromptDaemonExecutable();
}
public async startDaemonAndSelectRepository(repository: Repository) {
try {
let dialogContext = this.openStartupDialog(repository);
let daemonRunning = await this.repoService.checkDaemonRunning(
repository.path!);
if (!daemonRunning) {
dialogContext.message.next("Starting repository daemon...");
await this.repoService.startDaemon(repository.path!);
await new Promise((res, _) => {
setTimeout(res, 2000); // wait for the daemon to start
});
}
await this.selectRepository(repository, dialogContext);
} catch (err: any) {
this.logger.error(err);
}
}
public async selectRepository(repository: Repository, dialogContext?: BusyDialogContext) {
dialogContext = dialogContext ?? this.openStartupDialog(repository);
try {
dialogContext.message.next("Opening repository...");
await this.repoService.setRepository(repository);
await this.runRepositoryStartupTasks(dialogContext);
dialogContext.message.next("Restoring previous tabs...");
await this.repoService.loadRepositories();
dialogContext.dialog.close(true);
} catch (err: any) {
this.logger.error(err);
dialogContext.message.next(
"Failed to open repository: " + err.toString());
await this.forceCloseRepository();
setTimeout(() => dialogContext!.dialog.close(true), 1000);
}
}
public openAddRepositoryDialog() {
this.dialog.open(AddRepositoryDialogComponent, {
disableClose: true,
minWidth: "30%",
minHeight: "30%",
});
}
public async onOpenRepository(repository: Repository) {
if (!repository.local) {
await this.selectRepository(repository);
} else {
await this.startDaemonAndSelectRepository(repository);
}
}
public openAboutDialog(): void {
this.dialog.open(AboutDialogComponent, {
minWidth: "30%",
minHeight: "50%",
});
}
private async forceCloseRepository() {
try {
await this.repoService.closeSelectedRepository();
} catch {
}
try {
await this.repoService.disconnectSelectedRepository();
} catch {
}
}
private async runRepositoryStartupTasks(dialogContext: BusyDialogContext): Promise<void> {
dialogContext.message.next("Checking integrity...");
await this.jobService.runJob("CheckIntegrity");
dialogContext.message.next("Running a vacuum on the database...");
await this.jobService.runJob("Vacuum");
dialogContext.message.next(
"Migrating content descriptors to new format...");
await this.jobService.runJob("MigrateContentDescriptors");
dialogContext.message.next("Calculating repository sizes...");
await this.jobService.runJob("CalculateSizes", false);
dialogContext.message.next("Generating missing thumbnails...");
await this.jobService.runJob("GenerateThumbnails");
dialogContext.message.next("Finished repository startup");
}
private openStartupDialog(repository: Repository): BusyDialogContext {
const dialogMessage = new BehaviorSubject<string>(
"Opening repository...");
let dialog = this.dialog.open(BusyDialogComponent, {
data: {
title: `Opening repository '${repository.name}'`,
message: dialogMessage,
allowCancel: true,
}, disableClose: true,
minWidth: "30%",
minHeight: "30%",
});
dialog.afterClosed().subscribe(async (result) => {
if (!result) {
await this.forceCloseRepository();
}
});
return { message: dialogMessage, dialog };
}
private async checkAndPromptDaemonExecutable() {
if (!await this.repoService.checkDameonConfigured()) {
const result = await this.dialog.open(
DownloadDaemonDialogComponent,
{
disableClose: true,
}
).afterClosed().toPromise();
if (result) {
// recursion avoidance
setTimeout(
async () => await this.checkAndPromptDaemonExecutable(), 0);
}
}
}
}

@ -25,6 +25,7 @@ import {MatRippleModule} from "@angular/material/core";
import {FlapButtonComponent} from "./flap-button/flap-button.component";
import {MiddleCenteredComponent} from "./middle-centered/middle-centered.component";
import {FormatBytesPipe} from "./pipes/format-bytes.pipe";
import {ExternalUrlComponent} from "./external-url/external-url.component";
@NgModule({
@ -44,6 +45,7 @@ import {FormatBytesPipe} from "./pipes/format-bytes.pipe";
FlapButtonComponent,
MiddleCenteredComponent,
FormatBytesPipe,
ExternalUrlComponent,
],
exports: [
ConfirmDialogComponent,
@ -60,6 +62,7 @@ import {FormatBytesPipe} from "./pipes/format-bytes.pipe";
FlapButtonComponent,
MiddleCenteredComponent,
FormatBytesPipe,
ExternalUrlComponent,
],
imports: [
CommonModule,

@ -0,0 +1,3 @@
<span (click)="this.openUrl()" class="highlight">
<ng-content></ng-content>
</span>

@ -0,0 +1,15 @@
@import "src/colors";
.highlight {
cursor: pointer;
color: $primary-lighter-20;
text-decoration: underline;
&:hover {
color: $primary-lighter-30;
}
&:active {
color: $accent;
}
}

@ -0,0 +1,25 @@
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {ExternalUrlComponent} from "./external-url.component";
describe("ExternalUrlComponent", () => {
let component: ExternalUrlComponent;
let fixture: ComponentFixture<ExternalUrlComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ExternalUrlComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ExternalUrlComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,31 @@
import {ChangeDetectionStrategy, Component, Input} from "@angular/core";
import {shell} from "@tauri-apps/api";
@Component({
selector: "app-external-url",
templateUrl: "./external-url.component.html",
styleUrls: ["./external-url.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ExternalUrlComponent {
@Input() href!: string;
private opening = false;
constructor() {
}
public async openUrl() {
if (this.opening) {
return;
}
this.opening = true;
try {
await shell.open(this.href);
} catch (err) {
console.error(err);
} finally {
this.opening = false;
}
}
}

@ -1,4 +1,6 @@
<div class="metadata-entry-inner">
<div [class.display-line]="this.display === 'line'"
[class.display-multiline]="this.display === 'multiline'"
class="metadata-entry-inner">
<span class="metadata-attribute">{{attributeName}}:</span>
<span class="metadata-value">
<ng-content></ng-content>

@ -1,9 +1,30 @@
.metadata-entry-inner {
width: 100%;
display: flex;
flex-direction: column;
&.display-line {
display: flex;
flex-direction: row;
.metadata-attribute, .metadata-value {
width: 50%;
}
.metadata-attribute {
text-align: right;
}
.metadata-value {
text-align: left;
}
}
&.display-multiline {
flex-direction: column;
}
}
.metadata-attribute {
opacity: 0.8;
text-overflow: ellipsis;
@ -15,3 +36,4 @@
word-break: break-all;
user-select: text;
}

@ -1,14 +1,15 @@
import {Component, Input} from "@angular/core";
import {ChangeDetectionStrategy, Component, Input} from "@angular/core";
@Component({
selector: "app-metadata-entry",
templateUrl: "./metadata-entry.component.html",
styleUrls: ["./metadata-entry.component.scss"]
styleUrls: ["./metadata-entry.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MetadataEntryComponent {
@Input() display: "multiline" | "line" = "multiline";
@Input() attributeName!: string;
@Input() value!: string;
constructor() {
}

@ -0,0 +1,124 @@
#!/bin/env python3
import shutil as shut
import os
from lib import *
from clean import clean
from check import check, check_daemon_tooling, check_ui_tooling
from typing import List
build_output = 'out'
verbose = False
ffmpeg = False
install_tooling = False
windows = os.name == 'nt'
def main():
opts = parse_args()
global install_tooling
global build_output
global verbose
global ffmpeg
global install_tooling
build_output = opts.output if opts.output else build_output
verbose = opts.verbose
ffmpeg = opts.ffmpeg
install_tooling = opts.install_tooling
build(opts.component, opts.bundles)
def parse_args():
import argparse
parser = argparse.ArgumentParser(description='Build mediarepo')
parser.add_argument(
'component', type=str, nargs='?', default='all', choices=['daemon', 'ui', 'all'])
parser.add_argument(
'--verbose', action='store_true', help='Verbose build')
parser.add_argument(
'--output', action='store', help='Build output directory')
parser.add_argument(
'--ffmpeg', action='store_true', help='Build with ffmpeg')
parser.add_argument('--install-tooling',
action='store_true', help='Install tooling')
parser.add_argument('--bundles', nargs='+',
help='UI bundles to build')
args = parser.parse_args()
return args
def build(component: str, bundles: List[str] = None):
'''Builds the selected component'''
clean()
create_output_dir()
if component == 'all':
check(install_tooling)
build_daemon()
build_ui(bundles)
elif component == 'daemon':
check_daemon_tooling()
build_daemon()
elif component == 'ui':
check_ui_tooling(install_tooling)
build_ui(bundles)
print('Build complete')
def build_daemon():
'''Builds daemon'''
cargo('fetch', 'mediarepo-daemon')
if not ffmpeg:
cargo('build --release --frozen --no-default-features', 'mediarepo-daemon')
else:
cargo('build --release --frozen', 'mediarepo-daemon')
if windows:
store_artifact('mediarepo-daemon/target/release/mediarepo-daemon.exe')
else:
store_artifact('mediarepo-daemon/target/release/mediarepo-daemon')
def build_ui(bundles: List[str] = None):
'''Builds UI'''
yarn('install', 'mediarepo-ui')
if bundles is not None:
cargo('tauri build --bundles ' + ' '.join(bundles), 'mediarepo-ui')
else:
cargo('tauri build ', 'mediarepo-ui')
if windows:
store_artifact(
'mediarepo-ui/src-tauri/target/release/mediarepo-ui.exe')
else:
store_artifact('mediarepo-ui/src-tauri/target/release/mediarepo-ui')
store_artifact('mediarepo-ui/src-tauri/target/release/bundle/')
def create_output_dir():
'''Creates build output directory'''
if not os.path.exists(build_output):
os.mkdir(build_output)
def store_artifact(path: str):
'''Stores a build artifact'''
print('Storing {}'.format(path))
if os.path.isdir(path):
shut.copytree(path, os.path.join(
build_output, os.path.basename(path)), dirs_exist_ok=True)
elif os.path.isfile(path):
shut.copy(path, build_output)
if __name__ == '__main__':
main()

@ -0,0 +1,57 @@
#!/bin/env python3
from lib import *
import argparse
import os
tauri_cli_version = '1.0.0-rc.5'
windows = os.name == 'nt'
def main():
opts = parse_args()
check(opts.install)
def parse_args():
'''Parses command line arguments'''
args = argparse.ArgumentParser(description='Build mediarepo')
args.add_argument('--install', action='store_true',
help='Install tools that can be installed automatically')
return args.parse_args()
def check(install_tooling: bool = False):
'''Checks dependencies'''
check_daemon_tooling()
check_ui_tooling(install_tooling)
print('All checks passed')
def check_daemon_tooling():
'''Checks dependencies for daemon'''
check_exec('clang')
check_exec('cargo')
def check_ui_tooling(install_tooling: bool = False):
'''Checks dependencies for UI'''
if not windows:
check_exec('wget')
check_exec('curl')
check_exec('file')
check_exec('clang')
check_exec('cargo')
check_exec('node')
check_exec('npm')
check_yarn(install_tooling)
check_ng(install_tooling)
if install_tooling:
install_tauri_cli(tauri_cli_version)
if __name__ == '__main__':
main()

@ -0,0 +1,27 @@
#!/bin/env python3
import os
import shutil as shut
import argparse
def main():
opts = parse_args()
clean(opts.output if opts.output else 'out')
def parse_args():
'''Parses command line arguments'''
args = argparse.ArgumentParser(description='Build mediarepo')
args.add_argument('--output', action='store', help='Build output directory')
return args.parse_args()
def clean(build_output: str = 'out'):
'''Removes build output'''
if os.path.exists(build_output):
shut.rmtree(build_output)
print('Cleaned')
if __name__ == '__main__':
main()

@ -0,0 +1,57 @@
import subprocess
import shutil as shut
def install_tauri_cli(version: str):
cargo('install tauri-cli --version ^{}'.format(version))
def check_ng(install: bool = False):
'''Checks if ng is available and installs it
if the install flag is set'''
if not check_exec('ng'):
if install:
npm('install -g @angular/cli')
else:
raise Exception('ng not found')
def check_yarn(install: bool = False):
'''Checks if yarn is available and installs it
if the install flag is set'''
if not check_exec('yarn'):
if install:
npm('install -g yarn')
else:
raise Exception('yarn not found')
def yarn(cmd: str, dir: str = None) -> str:
'''Executes yarn in a given directory'''
exec('yarn {}'.format(cmd), dir)
def cargo(cmd: str, dir: str = None):
'''Executes cargo in a given directory'''
exec('cargo {}'.format(cmd), dir)
def npm(cmd: str, dir: str = None) -> str:
'''Executes npm in a given directory'''
exec('npm {}'.format(cmd), dir)
def check_exec(name: str) -> bool:
'''Checks if a command is available'''
if shut.which(name) is None:
print('{} not found'.format(name))
return False
exec('{} --version'.format(name))
return True
def exec(cmd: str, dir: str = None) -> str:
'''Executes a command in a given directory'''
print('Running: {}'.format(cmd))
child = subprocess.run(cmd, shell=True, cwd=dir)
child.check_returncode()
Loading…
Cancel
Save