Compare commits
91 Commits
v1.0.0-rc.
...
main
@ -0,0 +1,42 @@
|
||||
# compiled output
|
||||
out
|
||||
__pycache__
|
||||
target
|
||||
dist
|
||||
|
||||
# IDEs and editors
|
||||
mediarepo-api/.idea
|
||||
mediarepo-daemon/.idea
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# other
|
||||
*.zip
|
||||
.vscode
|
||||
|
||||
# ui
|
||||
mediarepo-ui/.angular
|
||||
mediarepo-ui/.idea
|
||||
mediarepo-ui/dist
|
||||
mediarepo-ui/node_modules
|
||||
mediarepo-ui/src-tauri/target
|
||||
|
||||
# daemon
|
||||
mediarepo-daemon/.idea
|
||||
mediarepo-daemon/target
|
||||
mediarepo-daemon/*.svg
|
||||
mediarepo-daemon/*.folded
|
||||
|
||||
# api
|
||||
mediarepo-api/.idea
|
||||
mediarepo-api/target
|
@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[bug] "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**System (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[feature]"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
me@trivernis.net.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
@ -0,0 +1,87 @@
|
||||
# Contributing
|
||||
|
||||
Before contributing to this repository, please discuss the changes you're willing to make via an issue or on discord before
|
||||
making those changes.
|
||||
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. Always submit Pull Requests to the develop branch unless it contains a hotfix
|
||||
2. Increase the API version according to the [SemVer](https://semver.org/) rules if you've done changes on the API
|
||||
3. Make sure that the project can be built and passes all automatic tests
|
||||
4. Your Pull Request will be merged after approval
|
||||
|
||||
|
||||
# Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
me@trivernis.net.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
|
@ -0,0 +1,45 @@
|
||||
ARG BASE_IMAGE=docker.io/alpine:latest
|
||||
|
||||
FROM ${BASE_IMAGE} AS base
|
||||
RUN apk update
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
openssl3-dev \
|
||||
gtk+3.0-dev \
|
||||
libappindicator-dev \
|
||||
patchelf \
|
||||
librsvg-dev \
|
||||
curl \
|
||||
wget \
|
||||
clang \
|
||||
nodejs \
|
||||
npm \
|
||||
libsoup-dev \
|
||||
webkit2gtk-dev \
|
||||
file \
|
||||
python3 \
|
||||
bash \
|
||||
protoc
|
||||
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
RUN rm -rf /var/lib/{cache,log}/ /var/cache
|
||||
|
||||
FROM base AS sources
|
||||
WORKDIR /usr/src
|
||||
COPY mediarepo-api ./mediarepo-api
|
||||
COPY mediarepo-daemon ./mediarepo-daemon
|
||||
COPY mediarepo-ui ./mediarepo-ui
|
||||
COPY scripts ./scripts
|
||||
RUN python3 scripts/clean.py
|
||||
RUN python3 scripts/check.py --install
|
||||
|
||||
FROM sources AS build_daemon
|
||||
WORKDIR /usr/src
|
||||
RUN python3 scripts/build.py daemon --verbose
|
||||
RUN mkdir ./test-repo
|
||||
RUN ./out/mediarepo-daemon --repo ./test-repo init
|
||||
|
||||
|
||||
FROM sources AS build_ui
|
||||
WORKDIR /usr/src
|
||||
RUN python3 scripts/build.py ui --verbose --bundles deb
|
@ -0,0 +1,11 @@
|
||||
[language-server.rust-analyzer]
|
||||
command = "rust-analyzer"
|
||||
|
||||
[language-server.rust-analyzer.config]
|
||||
inlayHints.bindingModeHints.enable = false
|
||||
inlayHints.closingBraceHints.minLines = 10
|
||||
inlayHints.closureReturnTypeHints.enable = "with_block"
|
||||
inlayHints.discriminantHints.enable = "fieldless"
|
||||
inlayHints.lifetimeElisionHints.enable = "skip_trivial"
|
||||
inlayHints.typeHints.hideClosureInitialization = false
|
||||
cargo.features = "all"
|
@ -1,16 +0,0 @@
|
||||
<h1 align="center">
|
||||
mediarepo-api
|
||||
</h1>
|
||||
<h3 align="center" color="red">
|
||||
This project is a work in progress
|
||||
</h3>
|
||||
|
||||
- - -
|
||||
|
||||
This repository contains common mediarepo API types to implement the API both serverside
|
||||
and clientside. It also contains a tauri plugin (feature "tauri-plugin") to hook it
|
||||
into the tauri application lifecycle.
|
||||
|
||||
## License
|
||||
|
||||
GPL-3
|
File diff suppressed because it is too large
Load Diff
@ -1,35 +0,0 @@
|
||||
<h1 align="center">
|
||||
mediarepo-daemon
|
||||
</h1>
|
||||
<p align="center">
|
||||
<img src="https://github.com/Trivernis/mediarepo-ui/raw/main/src-tauri/icons/64x64.png"/>
|
||||
</p>
|
||||
<h3 align="center" style="color:red">This repository is a work in progress</h3>
|
||||
|
||||
- - -
|
||||
|
||||
This repository contains a media repository daemon that allows one to manage their media.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Initialize an empty repository
|
||||
```
|
||||
mediarepo --repo "where/your/repo/should/be" init
|
||||
```
|
||||
|
||||
2. Import some images
|
||||
```
|
||||
mediarepo --repo "path/to/your/repo" import "path/to/your/files/as/**/glob/*.png"
|
||||
```
|
||||
|
||||
3. Start the daemon
|
||||
```
|
||||
mediarepo --repo "path/to/your/repo start
|
||||
```
|
||||
|
||||
4. Open the mediarepo-ui and connect to the repository
|
||||
|
||||
|
||||
## License
|
||||
|
||||
GPL-3
|
@ -1,14 +1,17 @@
|
||||
pub use bincode;
|
||||
pub use futures;
|
||||
pub use itertools;
|
||||
pub use mediarepo_api;
|
||||
pub use mediarepo_api::bromine;
|
||||
pub use thumbnailer;
|
||||
pub use tokio_graceful_shutdown;
|
||||
pub use trait_bound_typemap;
|
||||
|
||||
pub mod content_descriptor;
|
||||
pub mod context;
|
||||
pub mod error;
|
||||
pub mod fs;
|
||||
pub mod settings;
|
||||
pub mod tracing_layer_list;
|
||||
pub mod type_keys;
|
||||
pub mod utils;
|
||||
|
@ -0,0 +1,124 @@
|
||||
use std::slice::{Iter, IterMut};
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing::span::{Attributes, Record};
|
||||
use tracing::subscriber::Interest;
|
||||
use tracing::{Event, Id, Metadata, Subscriber};
|
||||
use tracing_subscriber::Layer;
|
||||
|
||||
pub struct DynLayerList<S>(Vec<Box<dyn Layer<S> + Send + Sync + 'static>>);
|
||||
|
||||
impl<S> Default for DynLayerList<S> {
|
||||
fn default() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> DynLayerList<S> {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> Iter<'_, Box<dyn Layer<S> + Send + Sync>> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> IterMut<'_, Box<dyn Layer<S> + Send + Sync>> {
|
||||
self.0.iter_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> DynLayerList<S>
|
||||
where
|
||||
S: Subscriber,
|
||||
{
|
||||
pub fn add<L: Layer<S> + Send + Sync>(&mut self, layer: L) {
|
||||
self.0.push(Box::new(layer));
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for DynLayerList<S>
|
||||
where
|
||||
S: Subscriber,
|
||||
{
|
||||
fn on_layer(&mut self, subscriber: &mut S) {
|
||||
self.iter_mut().for_each(|l| l.on_layer(subscriber));
|
||||
}
|
||||
|
||||
fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
|
||||
// Return highest level of interest.
|
||||
let mut interest = Interest::never();
|
||||
for layer in &self.0 {
|
||||
let new_interest = layer.register_callsite(metadata);
|
||||
if (interest.is_sometimes() && new_interest.is_always())
|
||||
|| (interest.is_never() && !new_interest.is_never())
|
||||
{
|
||||
interest = new_interest;
|
||||
}
|
||||
}
|
||||
interest
|
||||
}
|
||||
|
||||
fn enabled(
|
||||
&self,
|
||||
metadata: &Metadata<'_>,
|
||||
ctx: tracing_subscriber::layer::Context<'_, S>,
|
||||
) -> bool {
|
||||
self.iter().any(|l| l.enabled(metadata, ctx.clone()))
|
||||
}
|
||||
|
||||
fn on_new_span(
|
||||
&self,
|
||||
attrs: &Attributes<'_>,
|
||||
id: &Id,
|
||||
ctx: tracing_subscriber::layer::Context<'_, S>,
|
||||
) {
|
||||
self.iter()
|
||||
.for_each(|l| l.on_new_span(attrs, id, ctx.clone()));
|
||||
}
|
||||
|
||||
fn max_level_hint(&self) -> Option<LevelFilter> {
|
||||
self.iter().filter_map(|l| l.max_level_hint()).max()
|
||||
}
|
||||
|
||||
fn on_record(
|
||||
&self,
|
||||
span: &Id,
|
||||
values: &Record<'_>,
|
||||
ctx: tracing_subscriber::layer::Context<'_, S>,
|
||||
) {
|
||||
self.iter()
|
||||
.for_each(|l| l.on_record(span, values, ctx.clone()));
|
||||
}
|
||||
|
||||
fn on_follows_from(
|
||||
&self,
|
||||
span: &Id,
|
||||
follows: &Id,
|
||||
ctx: tracing_subscriber::layer::Context<'_, S>,
|
||||
) {
|
||||
self.iter()
|
||||
.for_each(|l| l.on_follows_from(span, follows, ctx.clone()));
|
||||
}
|
||||
|
||||
fn on_event(&self, event: &Event<'_>, ctx: tracing_subscriber::layer::Context<'_, S>) {
|
||||
self.iter().for_each(|l| l.on_event(event, ctx.clone()));
|
||||
}
|
||||
|
||||
fn on_enter(&self, id: &Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
|
||||
self.iter().for_each(|l| l.on_enter(id, ctx.clone()));
|
||||
}
|
||||
|
||||
fn on_exit(&self, id: &Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
|
||||
self.iter().for_each(|l| l.on_exit(id, ctx.clone()));
|
||||
}
|
||||
|
||||
fn on_close(&self, id: Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
|
||||
self.iter()
|
||||
.for_each(|l| l.on_close(id.clone(), ctx.clone()));
|
||||
}
|
||||
|
||||
fn on_id_change(&self, old: &Id, new: &Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
|
||||
self.iter()
|
||||
.for_each(|l| l.on_id_change(old, new, ctx.clone()));
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AsyncTryFrom<T> {
|
||||
type Error;
|
||||
fn async_try_from(other: T) -> Result<Self, Self::Error>;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
CREATE TABLE job_states (
|
||||
job_type INTEGER NOT NULL,
|
||||
value BLOB,
|
||||
PRIMARY KEY (job_type)
|
||||
);
|
@ -0,0 +1,45 @@
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::TryFromU64;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "job_states")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub job_type: JobType,
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, EnumIter, DeriveActiveEnum)]
|
||||
#[sea_orm(rs_type = "u32", db_type = "Integer")]
|
||||
pub enum JobType {
|
||||
#[sea_orm(num_value = 10)]
|
||||
MigrateCDs,
|
||||
#[sea_orm(num_value = 20)]
|
||||
CalculateSizes,
|
||||
#[sea_orm(num_value = 30)]
|
||||
GenerateThumbs,
|
||||
#[sea_orm(num_value = 40)]
|
||||
CheckIntegrity,
|
||||
#[sea_orm(num_value = 50)]
|
||||
Vacuum,
|
||||
}
|
||||
|
||||
impl TryFromU64 for JobType {
|
||||
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
|
||||
let value = match n {
|
||||
10 => Self::MigrateCDs,
|
||||
20 => Self::CalculateSizes,
|
||||
30 => Self::GenerateThumbs,
|
||||
40 => Self::CheckIntegrity,
|
||||
50 => Self::Vacuum,
|
||||
_ => return Err(DbErr::Custom(String::from("Invalid job type"))),
|
||||
};
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
@ -0,0 +1,58 @@
|
||||
use crate::dao::job::JobDao;
|
||||
use crate::dto::{JobStateDto, UpsertJobStateDto};
|
||||
use mediarepo_core::error::RepoResult;
|
||||
use mediarepo_database::entities::job_state;
|
||||
use mediarepo_database::entities::job_state::JobType;
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::{Condition, TransactionTrait};
|
||||
|
||||
impl JobDao {
|
||||
/// Returns all job states for a given job id
|
||||
pub async fn state_for_job_type(&self, job_type: JobType) -> RepoResult<Option<JobStateDto>> {
|
||||
let state = job_state::Entity::find()
|
||||
.filter(job_state::Column::JobType.eq(job_type))
|
||||
.one(&self.ctx.db)
|
||||
.await?
|
||||
.map(JobStateDto::new);
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
pub async fn upsert_state(&self, state: UpsertJobStateDto) -> RepoResult<()> {
|
||||
self.upsert_multiple_states(vec![state]).await
|
||||
}
|
||||
|
||||
pub async fn upsert_multiple_states(&self, states: Vec<UpsertJobStateDto>) -> RepoResult<()> {
|
||||
let trx = self.ctx.db.begin().await?;
|
||||
|
||||
job_state::Entity::delete_many()
|
||||
.filter(build_state_filters(&states))
|
||||
.exec(&trx)
|
||||
.await?;
|
||||
job_state::Entity::insert_many(build_active_state_models(states))
|
||||
.exec(&trx)
|
||||
.await?;
|
||||
|
||||
trx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn build_state_filters(states: &[UpsertJobStateDto]) -> Condition {
|
||||
states
|
||||
.iter()
|
||||
.map(|s| Condition::all().add(job_state::Column::JobType.eq(s.job_type)))
|
||||
.fold(Condition::any(), |acc, cond| acc.add(cond))
|
||||
}
|
||||
|
||||
fn build_active_state_models(states: Vec<UpsertJobStateDto>) -> Vec<job_state::ActiveModel> {
|
||||
states
|
||||
.into_iter()
|
||||
.map(|s| job_state::ActiveModel {
|
||||
job_type: Set(s.job_type),
|
||||
value: Set(s.value),
|
||||
})
|
||||
.collect()
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
use mediarepo_database::entities::job_state;
|
||||
use mediarepo_database::entities::job_state::JobType;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct JobStateDto {
|
||||
model: job_state::Model,
|
||||
}
|
||||
|
||||
impl JobStateDto {
|
||||
pub(crate) fn new(model: job_state::Model) -> Self {
|
||||
Self { model }
|
||||
}
|
||||
|
||||
pub fn job_type(&self) -> JobType {
|
||||
self.model.job_type
|
||||
}
|
||||
|
||||
pub fn value(&self) -> &[u8] {
|
||||
&self.model.value
|
||||
}
|
||||
|
||||
pub fn into_value(self) -> Vec<u8> {
|
||||
self.model.value
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UpsertJobStateDto {
|
||||
pub job_type: JobType,
|
||||
pub value: Vec<u8>,
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "mediarepo-worker"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.53"
|
||||
tracing = "0.1.33"
|
||||
|
||||
[dependencies.mediarepo-core]
|
||||
path = "../mediarepo-core"
|
||||
|
||||
[dependencies.mediarepo-logic]
|
||||
path = "../mediarepo-logic"
|
||||
|
||||
[dependencies.mediarepo-database]
|
||||
path = "../mediarepo-database"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.21.2"
|
||||
features = ["macros"]
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4.19"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.136"
|
||||
features = ["derive"]
|
@ -0,0 +1,101 @@
|
||||
use mediarepo_core::error::{RepoError, RepoResult};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::broadcast::{Receiver, Sender};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub struct JobHandle<T: Send + Sync, R: Send + Sync> {
|
||||
status: Arc<RwLock<T>>,
|
||||
state: Arc<RwLock<JobState>>,
|
||||
result_receiver: CloneableReceiver<Arc<RwLock<Option<RepoResult<R>>>>>,
|
||||
}
|
||||
|
||||
impl<T: Send + Sync, R: Send + Sync> Clone for JobHandle<T, R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
status: self.status.clone(),
|
||||
state: self.state.clone(),
|
||||
result_receiver: self.result_receiver.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync, R: Send + Sync> JobHandle<T, R> {
|
||||
pub fn new(
|
||||
status: Arc<RwLock<T>>,
|
||||
state: Arc<RwLock<JobState>>,
|
||||
result_receiver: CloneableReceiver<Arc<RwLock<Option<RepoResult<R>>>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
status,
|
||||
state,
|
||||
result_receiver,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn state(&self) -> JobState {
|
||||
*self.state.read().await
|
||||
}
|
||||
|
||||
pub fn status(&self) -> &Arc<RwLock<T>> {
|
||||
&self.status
|
||||
}
|
||||
|
||||
pub async fn result(&mut self) -> Arc<RwLock<Option<RepoResult<R>>>> {
|
||||
match self.result_receiver.recv().await {
|
||||
Ok(v) => v,
|
||||
Err(e) => Arc::new(RwLock::new(Some(Err(RepoError::from(&*e.to_string()))))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn take_result(&mut self) -> Option<RepoResult<R>> {
|
||||
let shared_result = self.result().await;
|
||||
let mut result = shared_result.write().await;
|
||||
result.take()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub enum JobState {
|
||||
Queued,
|
||||
Scheduled,
|
||||
Running,
|
||||
Finished,
|
||||
}
|
||||
|
||||
pub struct CloneableReceiver<T: Clone> {
|
||||
receiver: Receiver<T>,
|
||||
sender: Sender<T>,
|
||||
}
|
||||
|
||||
impl<T: Clone> CloneableReceiver<T> {
|
||||
pub fn new(sender: Sender<T>) -> Self {
|
||||
Self {
|
||||
receiver: sender.subscribe(),
|
||||
sender,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Clone for CloneableReceiver<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
sender: self.sender.clone(),
|
||||
receiver: self.sender.subscribe(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Deref for CloneableReceiver<T> {
|
||||
type Target = Receiver<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.receiver
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> DerefMut for CloneableReceiver<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.receiver
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
use crate::handle::{CloneableReceiver, JobHandle, JobState};
|
||||
use crate::jobs::{Job, JobTypeKey};
|
||||
use mediarepo_core::error::RepoError;
|
||||
use mediarepo_core::tokio_graceful_shutdown::SubsystemHandle;
|
||||
use mediarepo_core::trait_bound_typemap::{SendSyncTypeMap, TypeMap, TypeMapKey};
|
||||
use mediarepo_logic::dao::repo::Repo;
|
||||
use mediarepo_logic::dao::DaoProvider;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::broadcast::channel;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::time::Instant;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JobDispatcher {
|
||||
subsystem: SubsystemHandle,
|
||||
job_handle_map: Arc<RwLock<SendSyncTypeMap>>,
|
||||
repo: Arc<Repo>,
|
||||
}
|
||||
|
||||
impl JobDispatcher {
|
||||
pub fn new(subsystem: SubsystemHandle, repo: Repo) -> Self {
|
||||
Self {
|
||||
job_handle_map: Arc::new(RwLock::new(SendSyncTypeMap::new())),
|
||||
subsystem,
|
||||
repo: Arc::new(repo),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dispatch<T: 'static + Job>(&self, job: T) -> JobHandle<T::JobStatus, T::Result> {
|
||||
self._dispatch(job, None).await
|
||||
}
|
||||
|
||||
pub async fn dispatch_periodically<T: 'static + Job>(
|
||||
&self,
|
||||
job: T,
|
||||
interval: Duration,
|
||||
) -> JobHandle<T::JobStatus, T::Result> {
|
||||
self._dispatch(job, Some(interval)).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn _dispatch<T: 'static + Job>(
|
||||
&self,
|
||||
job: T,
|
||||
interval: Option<Duration>,
|
||||
) -> JobHandle<T::JobStatus, T::Result> {
|
||||
let status = job.status();
|
||||
let state = Arc::new(RwLock::new(JobState::Queued));
|
||||
let (sender, mut receiver) = channel(1);
|
||||
self.subsystem
|
||||
.start::<RepoError, _, _>("channel-consumer", move |subsystem| async move {
|
||||
tokio::select! {
|
||||
_ = receiver.recv() => (),
|
||||
_ = subsystem.on_shutdown_requested() => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
let receiver = CloneableReceiver::new(sender.clone());
|
||||
let handle = JobHandle::new(status.clone(), state.clone(), receiver);
|
||||
self.add_handle::<T>(handle.clone()).await;
|
||||
|
||||
let repo = self.repo.clone();
|
||||
|
||||
self.subsystem
|
||||
.start::<RepoError, _, _>("worker-job", move |subsystem| async move {
|
||||
loop {
|
||||
let start = Instant::now();
|
||||
let job_2 = job.clone();
|
||||
{
|
||||
let mut state = state.write().await;
|
||||
*state = JobState::Running;
|
||||
}
|
||||
if let Err(e) = job.load_state(repo.job()).await {
|
||||
tracing::error!("failed to load the jobs state: {}", e);
|
||||
}
|
||||
let result = tokio::select! {
|
||||
_ = subsystem.on_shutdown_requested() => {
|
||||
job_2.save_state(repo.job()).await
|
||||
}
|
||||
r = job.run(repo.clone()) => {
|
||||
match r {
|
||||
Err(e) => Err(e),
|
||||
Ok(v) => {
|
||||
let _ = sender.send(Arc::new(RwLock::new(Some(Ok(v)))));
|
||||
job.save_state(repo.job()).await
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Err(e) = result {
|
||||
tracing::error!("job failed with error: {}", e);
|
||||
let _ = sender.send(Arc::new(RwLock::new(Some(Err(e)))));
|
||||
}
|
||||
if let Some(interval) = interval {
|
||||
{
|
||||
let mut state = state.write().await;
|
||||
*state = JobState::Scheduled;
|
||||
}
|
||||
let sleep_duration = interval - start.elapsed();
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(sleep_duration) => {},
|
||||
_ = subsystem.on_shutdown_requested() => {break}
|
||||
}
|
||||
} else {
|
||||
let mut state = state.write().await;
|
||||
*state = JobState::Finished;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
handle
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn add_handle<T: 'static + Job>(&self, handle: JobHandle<T::JobStatus, T::Result>) {
|
||||
let mut status_map = self.job_handle_map.write().await;
|
||||
status_map.insert::<JobTypeKey<T>>(handle);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn get_handle<T: 'static + Job>(&self) -> Option<JobHandle<T::JobStatus, T::Result>> {
|
||||
let map = self.job_handle_map.read().await;
|
||||
map.get::<JobTypeKey<T>>().cloned()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DispatcherKey;
|
||||
|
||||
impl TypeMapKey for DispatcherKey {
|
||||
type Value = JobDispatcher;
|
||||
}
|
||||
|
||||
unsafe impl Send for JobDispatcher {}
|
||||
unsafe impl Sync for JobDispatcher {}
|
@ -0,0 +1,91 @@
|
||||
use crate::jobs::Job;
|
||||
use crate::status_utils::SimpleProgress;
|
||||
use async_trait::async_trait;
|
||||
use mediarepo_core::error::{RepoError, RepoResult};
|
||||
use mediarepo_core::mediarepo_api::types::repo::SizeType;
|
||||
use mediarepo_core::settings::Settings;
|
||||
use mediarepo_core::utils::get_folder_size;
|
||||
use mediarepo_logic::dao::repo::Repo;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tokio::fs;
|
||||
use tokio::sync::broadcast::{self, Sender};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub struct CalculateSizesState {
|
||||
pub progress: SimpleProgress,
|
||||
pub sizes_channel: Sender<(SizeType, u64)>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CalculateSizesJob {
|
||||
repo_path: PathBuf,
|
||||
settings: Settings,
|
||||
state: Arc<RwLock<CalculateSizesState>>,
|
||||
}
|
||||
|
||||
impl CalculateSizesJob {
|
||||
pub fn new(repo_path: PathBuf, settings: Settings) -> Self {
|
||||
let (tx, _) = broadcast::channel(4);
|
||||
Self {
|
||||
repo_path,
|
||||
settings,
|
||||
state: Arc::new(RwLock::new(CalculateSizesState {
|
||||
sizes_channel: tx,
|
||||
progress: SimpleProgress::new(4),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Job for CalculateSizesJob {
|
||||
type JobStatus = CalculateSizesState;
|
||||
type Result = ();
|
||||
|
||||
fn status(&self) -> Arc<RwLock<Self::JobStatus>> {
|
||||
self.state.clone()
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn run(&self, repo: Arc<Repo>) -> RepoResult<()> {
|
||||
let size_types = vec![
|
||||
SizeType::Total,
|
||||
SizeType::FileFolder,
|
||||
SizeType::ThumbFolder,
|
||||
SizeType::DatabaseFile,
|
||||
];
|
||||
for size_type in size_types {
|
||||
let size = calculate_size(&size_type, &repo, &self.repo_path, &self.settings).await?;
|
||||
let mut state = self.state.write().await;
|
||||
state
|
||||
.sizes_channel
|
||||
.send((size_type, size))
|
||||
.map_err(|_| RepoError::from("failed to broadcast new size"))?;
|
||||
state.progress.tick();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn calculate_size(
|
||||
size_type: &SizeType,
|
||||
repo: &Repo,
|
||||
repo_path: &Path,
|
||||
settings: &Settings,
|
||||
) -> RepoResult<u64> {
|
||||
let size = match &size_type {
|
||||
SizeType::Total => get_folder_size(repo_path.to_path_buf()).await?,
|
||||
SizeType::FileFolder => repo.get_main_store_size().await?,
|
||||
SizeType::ThumbFolder => repo.get_thumb_store_size().await?,
|
||||
SizeType::DatabaseFile => {
|
||||
let db_path = settings.paths.db_file_path(repo_path);
|
||||
|
||||
let database_metadata = fs::metadata(db_path).await?;
|
||||
database_metadata.len()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(size)
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
use crate::jobs::Job;
|
||||
use crate::status_utils::SimpleProgress;
|
||||
use async_trait::async_trait;
|
||||
use mediarepo_core::error::RepoResult;
|
||||
use mediarepo_logic::dao::repo::Repo;
|
||||
use mediarepo_logic::dao::DaoProvider;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CheckIntegrityJob {
|
||||
progress: Arc<RwLock<SimpleProgress>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Job for CheckIntegrityJob {
|
||||
type JobStatus = SimpleProgress;
|
||||
type Result = ();
|
||||
|
||||
fn status(&self) -> Arc<RwLock<Self::JobStatus>> {
|
||||
self.progress.clone()
|
||||
}
|
||||
|
||||
async fn run(&self, repo: Arc<Repo>) -> RepoResult<Self::Result> {
|
||||
repo.job().check_integrity().await?;
|
||||
{
|
||||
let mut progress = self.progress.write().await;
|
||||
progress.set_total(100);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
use crate::jobs::{deserialize_state, serialize_state, Job};
|
||||
use crate::status_utils::SimpleProgress;
|
||||
use async_trait::async_trait;
|
||||
use mediarepo_core::error::RepoResult;
|
||||
use mediarepo_core::thumbnailer::ThumbnailSize;
|
||||
use mediarepo_database::entities::job_state::JobType;
|
||||
use mediarepo_logic::dao::job::JobDao;
|
||||
use mediarepo_logic::dao::repo::Repo;
|
||||
use mediarepo_logic::dao::DaoProvider;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::mem;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct GenerateMissingThumbsJob {
|
||||
state: Arc<RwLock<SimpleProgress>>,
|
||||
inner_state: Arc<RwLock<GenerateThumbsState>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Job for GenerateMissingThumbsJob {
|
||||
type JobStatus = SimpleProgress;
|
||||
type Result = ();
|
||||
|
||||
fn status(&self) -> Arc<RwLock<Self::JobStatus>> {
|
||||
self.state.clone()
|
||||
}
|
||||
|
||||
async fn load_state(&self, job_dao: JobDao) -> RepoResult<()> {
|
||||
if let Some(state) = job_dao.state_for_job_type(JobType::GenerateThumbs).await? {
|
||||
let mut inner_state = self.inner_state.write().await;
|
||||
let state = deserialize_state::<GenerateThumbsState>(state)?;
|
||||
let _ = mem::replace(&mut *inner_state, state);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run(&self, repo: Arc<Repo>) -> RepoResult<()> {
|
||||
if !self.needs_generation(&repo).await? {
|
||||
return Ok(());
|
||||
}
|
||||
let file_dao = repo.file();
|
||||
let all_files = file_dao.all().await?;
|
||||
{
|
||||
let mut progress = self.state.write().await;
|
||||
progress.set_total(all_files.len() as u64);
|
||||
}
|
||||
|
||||
for file in all_files {
|
||||
if file_dao.thumbnails(file.encoded_cd()).await?.is_empty() {
|
||||
let _ = file_dao
|
||||
.create_thumbnails(&file, vec![ThumbnailSize::Medium])
|
||||
.await;
|
||||
}
|
||||
{
|
||||
let mut progress = self.state.write().await;
|
||||
progress.tick();
|
||||
}
|
||||
}
|
||||
|
||||
self.refresh_state(&repo).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_state(&self, job_dao: JobDao) -> RepoResult<()> {
|
||||
let state = self.inner_state.read().await;
|
||||
let state = serialize_state(JobType::GenerateThumbs, &*state)?;
|
||||
job_dao.upsert_state(state).await
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateMissingThumbsJob {
|
||||
async fn needs_generation(&self, repo: &Repo) -> RepoResult<bool> {
|
||||
let repo_counts = repo.get_counts().await?;
|
||||
let file_count = repo_counts.file_count as u64;
|
||||
let state = self.inner_state.read().await;
|
||||
|
||||
Ok(state.file_count != file_count
|
||||
|| state.last_run.elapsed().unwrap() > Duration::from_secs(60 * 60))
|
||||
}
|
||||
|
||||
async fn refresh_state(&self, repo: &Repo) -> RepoResult<()> {
|
||||
let repo_counts = repo.get_counts().await?;
|
||||
let mut state = self.inner_state.write().await;
|
||||
state.last_run = SystemTime::now();
|
||||
state.file_count = repo_counts.file_count as u64;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct GenerateThumbsState {
|
||||
file_count: u64,
|
||||
last_run: SystemTime,
|
||||
}
|
||||
|
||||
impl Default for GenerateThumbsState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
file_count: 0,
|
||||
last_run: SystemTime::now(),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
use crate::jobs::{deserialize_state, serialize_state, Job};
|
||||
use crate::status_utils::SimpleProgress;
|
||||
use async_trait::async_trait;
|
||||
use mediarepo_core::error::RepoResult;
|
||||
use mediarepo_database::entities::job_state::JobType;
|
||||
use mediarepo_logic::dao::job::JobDao;
|
||||
use mediarepo_logic::dao::repo::Repo;
|
||||
use mediarepo_logic::dao::DaoProvider;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MigrateCDsJob {
|
||||
progress: Arc<RwLock<SimpleProgress>>,
|
||||
migrated: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Job for MigrateCDsJob {
|
||||
type JobStatus = SimpleProgress;
|
||||
type Result = ();
|
||||
|
||||
fn status(&self) -> Arc<tokio::sync::RwLock<Self::JobStatus>> {
|
||||
self.progress.clone()
|
||||
}
|
||||
|
||||
async fn load_state(&self, job_dao: JobDao) -> RepoResult<()> {
|
||||
if let Some(state) = job_dao.state_for_job_type(JobType::MigrateCDs).await? {
|
||||
let state = deserialize_state::<MigrationStatus>(state)?;
|
||||
self.migrated.store(state.migrated, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run(&self, repo: Arc<Repo>) -> RepoResult<Self::Result> {
|
||||
if self.migrated.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
let job_dao = repo.job();
|
||||
|
||||
job_dao.migrate_content_descriptors().await?;
|
||||
self.migrated.store(true, Ordering::Relaxed);
|
||||
{
|
||||
let mut progress = self.progress.write().await;
|
||||
progress.set_total(100);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_state(&self, job_dao: JobDao) -> RepoResult<()> {
|
||||
if self.migrated.load(Ordering::Relaxed) {
|
||||
let state = serialize_state(JobType::MigrateCDs, &MigrationStatus { migrated: true })?;
|
||||
job_dao.upsert_state(state).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct MigrationStatus {
|
||||
pub migrated: bool,
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
mod calculate_sizes;
|
||||
mod check_integrity;
|
||||
mod generate_missing_thumbnails;
|
||||
mod migrate_content_descriptors;
|
||||
mod vacuum;
|
||||
|
||||
pub use calculate_sizes::*;
|
||||
pub use check_integrity::*;
|
||||
pub use generate_missing_thumbnails::*;
|
||||
pub use migrate_content_descriptors::*;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
pub use vacuum::*;
|
||||
|
||||
use crate::handle::JobHandle;
|
||||
use async_trait::async_trait;
|
||||
use mediarepo_core::bincode;
|
||||
use mediarepo_core::error::{RepoError, RepoResult};
|
||||
use mediarepo_core::trait_bound_typemap::TypeMapKey;
|
||||
use mediarepo_database::entities::job_state::JobType;
|
||||
use mediarepo_logic::dao::job::JobDao;
|
||||
use mediarepo_logic::dao::repo::Repo;
|
||||
use mediarepo_logic::dto::{JobStateDto, UpsertJobStateDto};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
type EmptyStatus = Arc<RwLock<()>>;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Job: Clone + Send + Sync {
|
||||
type JobStatus: Send + Sync;
|
||||
type Result: Send + Sync;
|
||||
|
||||
fn status(&self) -> Arc<RwLock<Self::JobStatus>>;
|
||||
|
||||
async fn load_state(&self, _job_dao: JobDao) -> RepoResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run(&self, repo: Arc<Repo>) -> RepoResult<Self::Result>;
|
||||
|
||||
async fn save_state(&self, _job_dao: JobDao) -> RepoResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JobTypeKey<T: Job>(PhantomData<T>);
|
||||
|
||||
impl<T: 'static> TypeMapKey for JobTypeKey<T>
|
||||
where
|
||||
T: Job,
|
||||
{
|
||||
type Value = JobHandle<T::JobStatus, T::Result>;
|
||||
}
|
||||
|
||||
pub fn deserialize_state<T: DeserializeOwned>(dto: JobStateDto) -> RepoResult<T> {
|
||||
bincode::deserialize(dto.value()).map_err(RepoError::from)
|
||||
}
|
||||
|
||||
pub fn serialize_state<T: Serialize>(
|
||||
job_type: JobType,
|
||||
state: &T,
|
||||
) -> RepoResult<UpsertJobStateDto> {
|
||||
let dto = UpsertJobStateDto {
|
||||
value: bincode::serialize(state)?,
|
||||
job_type,
|
||||
};
|
||||
|
||||
Ok(dto)
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
use crate::jobs::{EmptyStatus, Job};
|
||||
use async_trait::async_trait;
|
||||
use mediarepo_core::error::RepoResult;
|
||||
use mediarepo_logic::dao::repo::Repo;
|
||||
use mediarepo_logic::dao::DaoProvider;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct VacuumJob;
|
||||
|
||||
#[async_trait]
|
||||
impl Job for VacuumJob {
|
||||
type JobStatus = ();
|
||||
type Result = ();
|
||||
|
||||
fn status(&self) -> Arc<RwLock<Self::JobStatus>> {
|
||||
EmptyStatus::default()
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn run(&self, repo: Arc<Repo>) -> RepoResult<()> {
|
||||
repo.job().vacuum().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
use crate::job_dispatcher::JobDispatcher;
|
||||
use crate::jobs::{CheckIntegrityJob, MigrateCDsJob};
|
||||
use mediarepo_core::error::RepoError;
|
||||
use mediarepo_core::tokio_graceful_shutdown::Toplevel;
|
||||
use mediarepo_logic::dao::repo::Repo;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::oneshot::channel;
|
||||
|
||||
pub mod handle;
|
||||
pub mod job_dispatcher;
|
||||
pub mod jobs;
|
||||
pub mod status_utils;
|
||||
|
||||
pub async fn start(top_level: Toplevel, repo: Repo) -> (Toplevel, JobDispatcher) {
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let top_level =
|
||||
top_level.start::<RepoError, _, _>("mediarepo-worker", |subsystem| async move {
|
||||
let dispatcher = JobDispatcher::new(subsystem, repo);
|
||||
tx.send(dispatcher.clone())
|
||||
.map_err(|_| RepoError::from("failed to send dispatcher"))?;
|
||||
dispatcher
|
||||
.dispatch_periodically(
|
||||
CheckIntegrityJob::default(),
|
||||
Duration::from_secs(60 * 60 * 24),
|
||||
)
|
||||
.await;
|
||||
dispatcher.dispatch(MigrateCDsJob::default()).await;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
let receiver = rx
|
||||
.await
|
||||
.expect("failed to create background job dispatcher");
|
||||
|
||||
(top_level, receiver)
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
pub struct SimpleProgress {
|
||||
pub current: u64,
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
impl Default for SimpleProgress {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
total: 100,
|
||||
current: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SimpleProgress {
|
||||
pub fn new(total: u64) -> Self {
|
||||
Self { total, current: 0 }
|
||||
}
|
||||
|
||||
/// Sets the total count
|
||||
pub fn set_total(&mut self, total: u64) {
|
||||
self.total = total;
|
||||
}
|
||||
|
||||
/// Increments the current progress by 1
|
||||
pub fn tick(&mut self) {
|
||||
self.current += 1;
|
||||
}
|
||||
|
||||
/// Sets the current progress to a defined value
|
||||
pub fn set_current(&mut self, current: u64) {
|
||||
self.current = current;
|
||||
}
|
||||
|
||||
/// Returns the total progress in percent
|
||||
pub fn percent(&self) -> f64 {
|
||||
(self.current as f64) / (self.total as f64)
|
||||
}
|
||||
}
|
@ -1,66 +1,57 @@
|
||||
{
|
||||
"root": true,
|
||||
"ignorePatterns": [
|
||||
"projects/**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"tsconfig.json"
|
||||
],
|
||||
"createDefaultProgram": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/recommended",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "app",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "app",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
],
|
||||
"quotes": [
|
||||
"warn",
|
||||
"double",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"indent": [
|
||||
"error",
|
||||
4,
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"no-unused-expressions": "warn",
|
||||
"semi": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/template/recommended"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
"root": true,
|
||||
"ignorePatterns": ["projects/**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
"parserOptions": {
|
||||
"project": ["tsconfig.json"],
|
||||
"createDefaultProgram": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/recommended",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "app",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "app",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
],
|
||||
"quotes": [
|
||||
"warn",
|
||||
"double",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"indent": [
|
||||
"error",
|
||||
4,
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"no-unused-expressions": "warn",
|
||||
"no-extraneous-class": "off",
|
||||
"semi": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"extends": ["plugin:@angular-eslint/template/recommended"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
[language-server.biome]
|
||||
command = "biome"
|
||||
args = ["lsp-proxy"]
|
||||
|
||||
[[language]]
|
||||
name = "typescript"
|
||||
language-servers = ["typescript-language-server"]
|
||||
auto-format = true
|
||||
formatter = { command = "biome" , args = ["format", "--stdin-file-path=file.ts"] }
|
||||
|
||||
[[language]]
|
||||
name = "javascript"
|
||||
language-servers = ["typescript-language-server", "biome"]
|
||||
auto-format = true
|
||||
formatter = { command = "biome" , args = ["format", "--stdin-file-path=file.js"] }
|
@ -1,33 +0,0 @@
|
||||
<h1 align="center">
|
||||
mediarepo-ui
|
||||
</h1>
|
||||
<p align="center">
|
||||
<img src="https://github.com/Trivernis/mediarepo-ui/raw/main/src-tauri/icons/64x64.png"/>
|
||||
</p>
|
||||
<h3 align="center" style="color:red">This repository is a work in progress</h3>
|
||||
|
||||
- - -
|
||||
|
||||
This repository contains a frontend client to connect to
|
||||
the [mediarepo-daemon](../mediarepo-daemon). It is written in tauri (yay).
|
||||
|
||||
## Usage
|
||||
|
||||
Refer to [the tauri documentation](https://tauri.studio/en/docs/getting-started) for information about setting up your
|
||||
environment to build this project. With the `cargo-tauri` tooling installed you can run
|
||||
|
||||
```
|
||||
cargo tauri dev
|
||||
```
|
||||
|
||||
to start the application in development mode or
|
||||
|
||||
```
|
||||
cargo tauri build
|
||||
```
|
||||
|
||||
to bundle the application.
|
||||
|
||||
## License
|
||||
|
||||
GPL-3
|
@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.3.3/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"complexity": {
|
||||
"noStaticOnlyClass": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,62 +1,64 @@
|
||||
{
|
||||
"name": "mediarepo-ui",
|
||||
"version": "1.0.0-rc.1",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"watch-prod": "ng build --watch --configuration production",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~13.1.2",
|
||||
"@angular/cdk": "^13.1.2",
|
||||
"@angular/common": "~13.1.2",
|
||||
"@angular/compiler": "~13.1.2",
|
||||
"@angular/core": "~13.1.2",
|
||||
"@angular/flex-layout": "^13.0.0-beta.36",
|
||||
"@angular/forms": "~13.1.2",
|
||||
"@angular/material": "^13.1.2",
|
||||
"@angular/platform-browser": "~13.1.2",
|
||||
"@angular/platform-browser-dynamic": "~13.1.2",
|
||||
"@angular/router": "~13.1.2",
|
||||
"@ng-icons/core": "^13.2.1",
|
||||
"@ng-icons/feather-icons": "^13.2.1",
|
||||
"@ng-icons/material-icons": "^13.2.1",
|
||||
"@tauri-apps/api": "^1.0.0-beta.8",
|
||||
"primeicons": "^5.0.0",
|
||||
"primeng": "^13.0.4",
|
||||
"rxjs": "~7.5.2",
|
||||
"tslib": "^2.3.1",
|
||||
"w3c-keys": "^1.0.3",
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~13.1.3",
|
||||
"@angular-eslint/builder": "^13.0.1",
|
||||
"@angular-eslint/eslint-plugin": "^13.0.1",
|
||||
"@angular-eslint/eslint-plugin-template": "^13.0.1",
|
||||
"@angular-eslint/schematics": "^13.0.1",
|
||||
"@angular-eslint/template-parser": "^13.0.1",
|
||||
"@angular/cli": "~13.1.3",
|
||||
"@angular/compiler-cli": "~13.1.2",
|
||||
"@tauri-apps/cli": "^1.0.0-beta.10",
|
||||
"@types/file-saver": "^2.0.4",
|
||||
"@types/jasmine": "~3.10.3",
|
||||
"@types/node": "^16.11.19",
|
||||
"@typescript-eslint/eslint-plugin": "5.9.1",
|
||||
"@typescript-eslint/parser": "^5.9.1",
|
||||
"eslint": "^8.6.0",
|
||||
"jasmine-core": "~4.0.0",
|
||||
"karma": "~6.3.10",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.1.0",
|
||||
"karma-jasmine": "~4.0.1",
|
||||
"karma-jasmine-html-reporter": "~1.7.0",
|
||||
"typescript": "~4.5.4"
|
||||
}
|
||||
}
|
||||
"name": "mediarepo-ui",
|
||||
"version": "1.0.5",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"watch-prod": "ng build --watch --configuration production",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~13.3.2",
|
||||
"@angular/cdk": "^13.3.2",
|
||||
"@angular/common": "~13.3.2",
|
||||
"@angular/compiler": "~13.3.2",
|
||||
"@angular/core": "~13.3.2",
|
||||
"@angular/flex-layout": "^13.0.0-beta.36",
|
||||
"@angular/forms": "~13.3.2",
|
||||
"@angular/material": "^13.3.2",
|
||||
"@angular/platform-browser": "~13.3.2",
|
||||
"@angular/platform-browser-dynamic": "~13.3.2",
|
||||
"@angular/router": "~13.3.2",
|
||||
"@ng-icons/core": "^15.1.0",
|
||||
"@ng-icons/feather-icons": "^15.1.0",
|
||||
"@ng-icons/material-icons": "^15.1.0",
|
||||
"@tauri-apps/api": "^1.5.3",
|
||||
"chart.js": "^3.7.1",
|
||||
"primeicons": "^5.0.0",
|
||||
"primeng": "^13.3.2",
|
||||
"rxjs": "~7.5.5",
|
||||
"tslib": "^2.3.1",
|
||||
"w3c-keys": "^1.0.3",
|
||||
"zone.js": "~0.11.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~13.3.2",
|
||||
"@angular-eslint/builder": "^13.2.0",
|
||||
"@angular-eslint/eslint-plugin": "^13.2.0",
|
||||
"@angular-eslint/eslint-plugin-template": "^13.2.0",
|
||||
"@angular-eslint/schematics": "^13.2.0",
|
||||
"@angular-eslint/template-parser": "^13.2.0",
|
||||
"@angular/cli": "~13.3.2",
|
||||
"@angular/compiler-cli": "~13.3.2",
|
||||
"@angular/language-service": "^17.1.1",
|
||||
"@tauri-apps/cli": "^1.5.4",
|
||||
"@types/file-saver": "^2.0.4",
|
||||
"@types/jasmine": "~4.0.2",
|
||||
"@types/node": "^17.0.23",
|
||||
"@typescript-eslint/eslint-plugin": "5.19.0",
|
||||
"@typescript-eslint/parser": "^5.19.0",
|
||||
"eslint": "^8.13.0",
|
||||
"jasmine-core": "~4.0.0",
|
||||
"karma": "~6.3.10",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~4.0.2",
|
||||
"karma-jasmine-html-reporter": "~1.7.0",
|
||||
"typescript": "~4.6.3"
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,76 +1,74 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "mediarepo-ui",
|
||||
"version": "1.0.0-rc.1"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist/mediarepo-ui",
|
||||
"devPath": "http://localhost:4200",
|
||||
"beforeDevCommand": "yarn start",
|
||||
"beforeBuildCommand": "yarn build"
|
||||
},
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "net.trivernis.mediarepo",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/64x64.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.ico",
|
||||
"icons/icon.icns"
|
||||
],
|
||||
"resources": [],
|
||||
"externalBin": [],
|
||||
"copyright": "",
|
||||
"category": "Productivity",
|
||||
"shortDescription": "A media management tool",
|
||||
"longDescription": "",
|
||||
"deb": {
|
||||
"depends": [],
|
||||
"useBootstrapper": false
|
||||
},
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "",
|
||||
"useBootstrapper": false,
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": null,
|
||||
"entitlements": null
|
||||
},
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
},
|
||||
"allowlist": {
|
||||
"dialog": {
|
||||
"all": true
|
||||
},
|
||||
"shell": {
|
||||
"all": true
|
||||
},
|
||||
"path": {
|
||||
"all": true
|
||||
}
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "mediarepo",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self' once: thumb: content:"
|
||||
}
|
||||
}
|
||||
}
|
||||
"package": {
|
||||
"productName": "mediarepo-ui",
|
||||
"version": "1.0.4"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist/mediarepo-ui",
|
||||
"devPath": "http://localhost:4200",
|
||||
"beforeDevCommand": "yarn start",
|
||||
"beforeBuildCommand": "yarn build"
|
||||
},
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["msi", "app", "dmg", "updater"],
|
||||
"identifier": "net.trivernis.mediarepo",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/64x64.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.ico",
|
||||
"icons/icon.icns"
|
||||
],
|
||||
"resources": [],
|
||||
"externalBin": [],
|
||||
"copyright": "",
|
||||
"category": "Productivity",
|
||||
"shortDescription": "A media management tool",
|
||||
"longDescription": "",
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "",
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": null,
|
||||
"entitlements": null
|
||||
},
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
},
|
||||
"allowlist": {
|
||||
"dialog": {
|
||||
"all": true
|
||||
},
|
||||
"shell": {
|
||||
"all": true
|
||||
},
|
||||
"path": {
|
||||
"all": true
|
||||
}
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "mediarepo",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue