Initial commit
This commit is contained in:
commit
7523a19d1f
40 changed files with 3984 additions and 0 deletions
40
templates/base.html
Normal file
40
templates/base.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
{% from 'bootstrap/utils.html' import render_messages %}
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{% block head %}
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
{% block styles %}
|
||||
{{ bootstrap.load_css() }}
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='theme.css')}}">
|
||||
{% endblock %}
|
||||
<title>MediaDash</title>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block navbar %}
|
||||
<nav class="navbar sticky-top navbar-expand-lg navbar-dark" style="background-color: #222;">
|
||||
<a class="navbar-brand" href="/">MediaDash</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar_main" aria-controls="navbar_main" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbar_main">
|
||||
{{nav.left_nav.render(renderer='bootstrap4')}}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class={{"container-fluid" if fluid else "container"}}>
|
||||
{{render_messages()}}
|
||||
{% block app_content %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ bootstrap.load_js(with_popper=False) }}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
70
templates/config.html
Normal file
70
templates/config.html
Normal file
|
@ -0,0 +1,70 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'utils.html' import custom_render_form_row,make_tabs %}
|
||||
{% from 'bootstrap/form.html' import render_form, render_field, render_form_row %}
|
||||
|
||||
{% set col_size = ('lg',2,6) %}
|
||||
{% set col_size_seq = ('lg',10,1) %}
|
||||
|
||||
{% macro render_fields(fields) %}
|
||||
{% for field in fields %}
|
||||
{% if field is sequence %}
|
||||
{{ custom_render_form_row(field|list,col_map={'transcode_edit':('lg',1),'transcode_new':('lg',1)},render_args={'form_type':'horizontal'}) }}
|
||||
{% else %}
|
||||
{{ custom_render_form_row([field],render_args={'form_type':'horizontal','horizontal_columns':col_size}) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
{% set config_tabs = [] %}
|
||||
{% for name, fields in [
|
||||
('Jellyfin',[form.jellyfin_url,form.jellyfin_username,form.jellyfin_passwd]),
|
||||
('QBittorrent',[form.qbt_url,form.qbt_username,form.qbt_passwd]),
|
||||
('Sonarr',[form.sonarr_url,form.sonarr_api_key]),
|
||||
('Radarr',[form.radarr_url,form.radarr_api_key]),
|
||||
('Portainer',[form.portainer_url,form.portainer_username,form.portainer_passwd]),
|
||||
('Jackett',[form.jackett_url,form.jackett_api_key]),
|
||||
('Transcode',[form.transcode_default_profile,form.transcode_profiles]),
|
||||
] %}
|
||||
{% do config_tabs.append((name,render_fields(fields))) %}
|
||||
{% endfor %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>{{title}}</h1>
|
||||
{% if test %}
|
||||
{% if test.success %}
|
||||
<div class="alert alert-success" role="danger">
|
||||
<h4>Sucess</h4>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-danger" role="danger">
|
||||
{% for module,error in test.errors.items() %}
|
||||
{% if error %}
|
||||
<h4>{{module}}</h4>
|
||||
{% if error is mapping %}
|
||||
{% for key,value in error.items() %}
|
||||
<p><b>{{key}}</b>: {{value}}</p>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<b>{{error}}</b>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<div class="alert alert-danger" role="danger">{{error}}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<form method="post" class="form" enctype="multipart/form-data">
|
||||
{{ form.csrf_token() }}
|
||||
{{ make_tabs(config_tabs) }}
|
||||
{{ custom_render_form_row([form.test, form.save],button_map={'test':'primary','save':'success'},col_map={'test':0,'primary':0},render_args={'form_type':'horizontal'})}}
|
||||
</form>
|
||||
{# render_form(form, form_type ="horizontal", button_map={'test':'primary','save':'success'}) #}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
14
templates/containers/details.html
Normal file
14
templates/containers/details.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>
|
||||
<a href="{{config.APP_CONFIG.portainer_url}}#/containers/{{container.Id}}">
|
||||
{{container.Config.Labels["com.docker.compose.project"]}}/{{container.Config.Labels["com.docker.compose.service"]}}
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<h4>Env</h4>
|
||||
<pre>{{container.Config.Env|join("\n")}}</pre>
|
||||
|
||||
<pre>{{container|tojson(indent=4)}}</pre>
|
||||
{% endblock %}
|
58
templates/containers/index.html
Normal file
58
templates/containers/index.html
Normal file
|
@ -0,0 +1,58 @@
|
|||
{% extends "base.html" %}
|
||||
{% from "utils.html" import make_tabs %}
|
||||
|
||||
{% macro container_row(info) %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Image
|
||||
</div>
|
||||
<div class="col">
|
||||
<a href="{{urljoin('https://hub.docker.com/r/',info.Image)}}">{{info.Image}}</a>
|
||||
</div>
|
||||
<div class="col">
|
||||
Status
|
||||
</div>
|
||||
<div class="col">
|
||||
{{info.Status}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Id: <a href="{{'{}#/containers/{}'.format(config.APP_CONFIG.portainer_url,info.Id)}}">{{info.Id}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<pre>{{info|tojson(indent=4)}}</pre>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>
|
||||
<a href="{{config.APP_CONFIG.portainer_url}}">Portainer</a>
|
||||
</h1>
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Image</th>
|
||||
<th scope="col">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for container in containers %}
|
||||
{% set label = container.Labels["com.docker.compose.service"] %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{url_for('containers',container_id=container.Id)}}">
|
||||
{{container.Labels["com.docker.compose.project"]}}/{{container.Labels["com.docker.compose.service"]}}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{urljoin('https://hub.docker.com/r/',container.Image)}}">{{container.Image}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{container.Status}}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
65
templates/history.html
Normal file
65
templates/history.html
Normal file
|
@ -0,0 +1,65 @@
|
|||
{%- extends "base.html" -%}
|
||||
{%- from 'utils.html' import make_tabs -%}
|
||||
|
||||
{%- macro default(event,source) -%}
|
||||
<h5>Unknown ({{source}})</h5>
|
||||
<pre>{{event|tojson(indent=4)}}</pre>
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro downloadFolderImported(event,source) -%}
|
||||
[{{event.seriesId}}/{{event.episodeId}}] Imported {{event.data.droppedPath}} from {{event.data.downloadClientName}} to {{event.data.importedPath}}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro grabbed(event,source) -%}
|
||||
[{{event.seriesId}}/{{event.episodeId}}] Grabbed <a href="{{event.data.guid}}">{{event.sourceTitle}}</a>
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro episodeFileDeleted(event,source) -%}
|
||||
[{{event.seriesId}}/{{event.episodeId}}] Deleted {{event.sourceTitle}} because {{event.data.reason}}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro episodeFileRenamed(event,source) -%}
|
||||
[{{event.seriesId}}/{{event.episodeId}}] Renamed {{event.data.sourcePath}} to {{event.data.path}}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro movieFileDeleted(event,source) -%}
|
||||
Renamed {{event.data.sourcePath}} to {{event.data.path}}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro movieFileRenamed(event,source) -%}
|
||||
<h5>renamed</h5>
|
||||
<pre>{{event|tojson(indent=4)}}</pre>
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro downloadFailed(event,source) -%}
|
||||
<h5>downloadFailed</h5>
|
||||
<pre>{{event|tojson(indent=4)}}</pre>
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- set handlers = {
|
||||
'downloadFolderImported': downloadFolderImported,
|
||||
'grabbed': grabbed,
|
||||
'episodeFileDeleted': episodeFileDeleted,
|
||||
'episodeFileRenamed': episodeFileRenamed,
|
||||
'movieFileDeleted': movieFileDeleted,
|
||||
'movieFileRenamed': movieFileRenamed,
|
||||
'downloadFailed': downloadFailed,
|
||||
None: default
|
||||
} -%}
|
||||
|
||||
{%- macro history_page(history,source) -%}
|
||||
<pre>
|
||||
{%- for entry in history.records -%}
|
||||
{{handlers.get(entry.eventType,handlers[None])(entry,source)}}{{'\n'}}
|
||||
{%- endfor -%}
|
||||
</pre>
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- block app_content -%}
|
||||
<h2>History</h2>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{make_tabs([('Sonarr',history_page(sonarr,'sonarr')),('Radarr',history_page(radarr,'radarr'))])}}
|
||||
</div>
|
||||
</div>
|
||||
{%- endblock -%}
|
122
templates/index.html
Normal file
122
templates/index.html
Normal file
|
@ -0,0 +1,122 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% macro make_row(title,items) %}
|
||||
<div class="d-flex flex-wrap">
|
||||
{% for item in items %}
|
||||
{{item|safe}}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro make_tabs(tabs) %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">
|
||||
{% for (label,_) in tabs %}
|
||||
{% set slug = (label|slugify) %}
|
||||
{% if not (loop.first and loop.last) %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{'active' if loop.first}}" id="nav-{{slug}}-tab" data-toggle="pill" href="#pills-{{slug}}" role="tab" aria-controls="pills-{{slug}}" aria-selected="{{loop.first}}">
|
||||
{{label}}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="searchResults">
|
||||
{% for (label,items) in tabs %}
|
||||
{% set slug = (label|slugify) %}
|
||||
<div class="tab-pane fade {{'show active' if loop.first}}" id="pills-{{slug}}" role="tabpanel" aria-labelledby="nav-{{slug}}-tab">
|
||||
{{make_row(label,items)}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro upcoming(data) %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<h3>Movies</h3>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>In Cinemas</th>
|
||||
<th>Digital Release</th>
|
||||
</tr>
|
||||
{% for movie in data.calendar.movies %}
|
||||
{% if movie.isAvailable and movie.hasFile %}
|
||||
{% set row_class = "bg-success" %}
|
||||
{% elif movie.isAvailable and not movie.hasFile %}
|
||||
{% set row_class = "bg-danger" %}
|
||||
{% elif not movie.isAvailable and movie.hasFile %}
|
||||
{% set row_class = "bg-primary" %}
|
||||
{% elif not movie.isAvailable and not movie.hasFile %}
|
||||
{% set row_class = "bg-info" %}
|
||||
{% endif %}
|
||||
<tr class={{row_class}}>
|
||||
<td>
|
||||
<a href="{{urljoin(config.APP_CONFIG.radarr_url,'movie/'+movie.titleSlug)}}" style="color: #eee; text-decoration: underline;">
|
||||
{{movie.title}}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{movie.inCinemas|fromiso|ago_dt_utc_human(rnd=0)}}</td>
|
||||
<td>{{movie.digitalRelease|fromiso|ago_dt_utc_human(rnd=0)}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<h3>Episodes</h3>
|
||||
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th>Season | Episode Number</th>
|
||||
<th>Show</th>
|
||||
<th>Title</th>
|
||||
<th>Air Date</th>
|
||||
</tr>
|
||||
{% for entry in data.calendar.episodes %}
|
||||
{% if entry.episode.hasAired and entry.episode.hasFile %}
|
||||
{% set row_class = "bg-success" %}
|
||||
{% elif entry.episode.hasAired and not entry.episode.hasFile %}
|
||||
{% set row_class = "bg-danger" %}
|
||||
{% elif not entry.episode.hasAired and entry.episode.hasFile %}
|
||||
{% set row_class = "bg-primary" %}
|
||||
{% elif not entry.episode.hasAired and not entry.episode.hasFile %}
|
||||
{% set row_class = "bg-info" %}
|
||||
{% endif %}
|
||||
<tr class={{row_class}}>
|
||||
<td>{{entry.episode.seasonNumber}} | {{entry.episode.episodeNumber}}</td>
|
||||
<td>
|
||||
<a href="{{urljoin(config.APP_CONFIG.sonarr_url,'series/'+entry.series.titleSlug)}}" style="color: #eee; text-decoration: underline;">
|
||||
{{entry.series.title}}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{entry.episode.title}}</td>
|
||||
<td>{{entry.episode.airDateUtc|fromiso|ago_dt_utc_human(rnd=0)}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% block app_content %}
|
||||
{% if data is none %}
|
||||
<h2>No Data available!</h2>
|
||||
{% else %}
|
||||
{% set tabs = [] %}
|
||||
{% do tabs.append(("Upcoming",[upcoming(data)])) %}
|
||||
{% for row in data.images %}
|
||||
{% if row[0] is string %}
|
||||
{% set title=row[0] %}
|
||||
{% set row=row[1:] %}
|
||||
{% do tabs.append((title,row)) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{make_tabs(tabs)}}
|
||||
{% endif %}
|
||||
{% endblock %}
|
121
templates/jellyfin/index.html
Normal file
121
templates/jellyfin/index.html
Normal file
|
@ -0,0 +1,121 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'utils.html' import custom_render_form_row,make_tabs %}
|
||||
{% from 'bootstrap/utils.html' import render_icon %}
|
||||
{% from 'bootstrap/form.html' import render_form, render_field, render_form_row %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<h2><a href={{jellyfin.info.LocalAddress}}>Jellyfin</a> v{{jellyfin.info.Version}}</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<h4>Active Streams</h4>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th>Episode</th>
|
||||
<th>Show</th>
|
||||
<th>Language</th>
|
||||
<th>User</th>
|
||||
<th>Device</th>
|
||||
<th>Mode</th>
|
||||
</tr>
|
||||
{% for session in jellyfin.sessions %}
|
||||
{% if "NowPlayingItem" in session %}
|
||||
{% with np=session.NowPlayingItem, ps=session.PlayState%}
|
||||
<tr>
|
||||
<td>
|
||||
{% if session.SupportsMediaControl %}
|
||||
<a href="{{url_for('stop_stream',session=session.Id)}}">
|
||||
{{render_icon("stop-circle")}}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{cfg().jellyfin_url}}web/index.html#!/details?id={{np.Id}}">
|
||||
{{np.Name}}
|
||||
</a>
|
||||
({{(ps.PositionTicks/10_000_000)|timedelta(digits=0)}}/{{(np.RunTimeTicks/10_000_000)|timedelta(digits=0)}})
|
||||
{% if ps.IsPaused %}
|
||||
(Paused)
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{cfg().jellyfin_url}}web/index.html#!/details?id={{np.SeriesId}}">
|
||||
{{np.SeriesName}}
|
||||
</a>
|
||||
<a href="{{cfg().jellyfin_url}}web/index.html#!/details?id={{np.SeasonId}}">
|
||||
({{np.SeasonName}})
|
||||
</a>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
{% if ("AudioStreamIndex" in ps) and ("SubtitleStreamIndex" in ps) %}
|
||||
{{np.MediaStreams[ps.AudioStreamIndex].Language or "None"}}/{{np.MediaStreams[ps.SubtitleStreamIndex].Language or "None"}}
|
||||
{% else %}
|
||||
Unk/Unk
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{cfg().jellyfin_url}}web/index.html#!/useredit.html?userId={{session.UserId}}">
|
||||
{{session.UserName}}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{session.DeviceName}}
|
||||
</td>
|
||||
<td>
|
||||
{% if ps.PlayMethod =="Transcode" %}
|
||||
<p title="{{session.TranscodingInfo.Bitrate|filesizeformat(binary=False)}}/s | {{session.TranscodingInfo.CompletionPercentage|round(2)}}%">
|
||||
{{ps.PlayMethod}}
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
{{ps.PlayMethod}}
|
||||
</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<h4>Users</h4>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Last Login</th>
|
||||
<th>Last Active</th>
|
||||
<th>Bandwidth Limit</th>
|
||||
</tr>
|
||||
{% for user in jellyfin.users|sort(attribute="LastLoginDate",reverse=True) %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{cfg().jellyfin_url}}web/index.html#!/useredit.html?userId={{user.Id}}">
|
||||
{{user.Name}}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if "LastLoginDate" in user %}
|
||||
{{user.LastLoginDate|fromiso|ago_dt_utc(2)}} ago
|
||||
{% else %}
|
||||
Never
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if "LastActivityDate" in user %}
|
||||
{{user.LastActivityDate|fromiso|ago_dt_utc(2)}} ago
|
||||
{% else %}
|
||||
Never
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{user.Policy.RemoteClientBitrateLimit|filesizeformat(binary=False)}}/s</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
39
templates/logs.html
Normal file
39
templates/logs.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<h2>QBittorrent</h2>
|
||||
<div class="monospace">
|
||||
{% set t_first = logs.qbt[0].timestamp %}
|
||||
{% for message in logs.qbt if "WebAPI login success" not in message.message %}
|
||||
{%set type={1: 'status' , 2: 'info', 4: 'warning', 8:'danger'}.get(message.type,none) %}
|
||||
{%set type_name={1: 'NORMAL' , 2: 'INFO', 4: 'WARNING', 8:'CRITICAL'}.get(message.type,none) %}
|
||||
<p class="text-{{type}}">
|
||||
[{{((message.timestamp-t_first)/1000) | timedelta}}|{{type_name}}] {{message.message.strip()}}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h2>Sonarr</h2>
|
||||
<div class="monospace">
|
||||
{% set t_first = (logs.sonarr.records[0].time)|fromiso %}
|
||||
{% for message in logs.sonarr.records %}
|
||||
{%set type={'warn': 'warning', 'error':'danger'}.get(message.level,message.level) %}
|
||||
<p class="text-{{type}}">
|
||||
[{{message.time | fromiso | ago_dt}}|{{message.logger}}|{{message.level|upper}}] {{message.message.strip()}}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h2>Radarr</h2>
|
||||
<div class="monospace">
|
||||
{% set t_first = (logs.radarr.records[0].time)|fromiso %}
|
||||
{% for message in logs.radarr.records %}
|
||||
{%set type={'warn': 'warning', 8:'danger'}.get(message.level,message.level) %}
|
||||
<p class="text-{{type}}">
|
||||
[{{message.time | fromiso | ago_dt}}|{{message.logger}}|{{message.level|upper}}] {{message.message.strip()}}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
236
templates/qbittorrent/details.html
Normal file
236
templates/qbittorrent/details.html
Normal file
|
@ -0,0 +1,236 @@
|
|||
{% extends "base.html" %}
|
||||
{% from "utils.html" import render_tree %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{{super()}}
|
||||
<script lang="text/javascript">
|
||||
var toggler = document.getElementsByClassName("custom_caret");
|
||||
var i;
|
||||
for (i = 0; i < toggler.length; i++) {
|
||||
toggler[i].addEventListener("click", function () {
|
||||
this.parentElement.querySelector(".nested").classList.toggle("active");
|
||||
this.classList.toggle("custom_caret-down");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>
|
||||
<a href="{{qbt.info.magnet_uri}}" title="{{qbt.info.hash}}">{{qbt.info.name}}</a>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="progress" style="width: 100%;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
style="width: {{(qbt.info.progress*100)|round(2)}}%;" role="progressbar"
|
||||
aria-valuenow="{{(qbt.info.progress*100)|round(2)}}" aria-valuemin="0" aria-valuemax="100">
|
||||
{{(qbt.info.progress*100)|round(2)}} %
|
||||
</div>
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated bg-primary"
|
||||
style="width: {{((([qbt.info.availability,1]|min)-qbt.info.progress)*100)|round(2)}}%;" role="progressbar"
|
||||
aria-valuenow="{{((([qbt.info.availability,1]|min)-qbt.info.progress)*100)|round(2)}}" aria-valuemin="0" aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<span class="badge badge-{{qbt.info.state[1]}}">
|
||||
{{qbt.info.state[0]}}
|
||||
</span>
|
||||
{% if qbt.info.category %}
|
||||
<span class="badge badge-light">{{qbt.info.category}}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Info</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Total Size
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.info.size|filesizeformat(binary=True)}} ({{[0,qbt.info.size-qbt.info.downloaded]|max|filesizeformat(binary=True)}} left)
|
||||
</div>
|
||||
<div class="col">
|
||||
Files
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.files|count}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Downloaded
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.info.downloaded|filesizeformat(binary=True)}} ({{qbt.info.dlspeed|filesizeformat(binary=True)}}/s)
|
||||
</div>
|
||||
<div class="col">
|
||||
Uploaded
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.info.uploaded|filesizeformat(binary=True)}} ({{qbt.info.upspeed|filesizeformat(binary=True)}}/s)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Health</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Last Active
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.info.last_activity|ago(clamp=True)}} Ago
|
||||
</div>
|
||||
<div class="col">
|
||||
Age
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.info.added_on|ago}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col">
|
||||
Avg. DL rate
|
||||
</div>
|
||||
<div class="col">
|
||||
{{(qbt.info.downloaded/((qbt.info.added_on|ago).total_seconds()))|filesizeformat(binary=True)}}/s
|
||||
(A: {{(qbt.info.downloaded/qbt.info.time_active)|filesizeformat(binary=True)}}/s)
|
||||
</div>
|
||||
<div class="col">
|
||||
Avg. UL rate
|
||||
</div>
|
||||
<div class="col">
|
||||
{{(qbt.info.uploaded/((qbt.info.added_on|ago).total_seconds()))|filesizeformat(binary=True)}}/s
|
||||
(A: {{(qbt.info.uploaded/qbt.info.time_active)|filesizeformat(binary=True)}}/s)
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
<div class="col">
|
||||
ETC (DL rate while active)
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
{% set dl_rate_act = (qbt.info.downloaded/qbt.info.time_active) %}
|
||||
{% if dl_rate_act>0 %}
|
||||
{{((qbt.info.size-qbt.info.downloaded)/dl_rate_act)|round(0)|timedelta(clamp=true)}}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
ETC (avg. DL rate)
|
||||
</div>
|
||||
<div class="col">
|
||||
{% set dl_rate = (qbt.info.downloaded/((qbt.info.added_on|ago(clamp=True)).total_seconds())) %}
|
||||
{% if dl_rate>0 %}
|
||||
{{((qbt.info.size-qbt.info.downloaded)/dl_rate)|round(0)|timedelta(clamp=true)}}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Total active time
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.info.time_active|timedelta}}
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
Availability
|
||||
</div>
|
||||
<div class="col">
|
||||
{% if qbt.info.availability==-1 %}
|
||||
N/A
|
||||
{% else %}
|
||||
{{(qbt.info.availability*100)|round(2)}} %
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Swarm</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Seeds
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.info.num_seeds}}
|
||||
</div>
|
||||
<div class="col">
|
||||
Leechers
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.info.num_leechs}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Last seen completed
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.info.seen_complete|ago}} Ago
|
||||
</div>
|
||||
<div class="col"></div>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<h2>Files</h2>
|
||||
|
||||
{{render_tree(qbt.files|sort(attribute='name')|list|make_tree)}}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h2>Trackers</h2>
|
||||
<a href="{{url_for('qbittorent_add_trackers',infohash=qbt.info.hash)}}">
|
||||
<span class="badge badge-primary">Add default trackers</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for tracker in qbt.trackers|sort(attribute='total_peers', reverse=true) %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% if tracker.has_url %}
|
||||
<a href="{{tracker.url}}">{{tracker.name}}</a>
|
||||
{% else %}
|
||||
{{tracker.name}}
|
||||
{% endif %}
|
||||
{% if tracker.message %}
|
||||
<code>{{tracker.message}}</code>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col">
|
||||
<span class="badge badge-{{tracker.status[1]}}">{{tracker.status[0]}}</span>
|
||||
(S: {{tracker.num_seeds[1]}}, L: {{tracker.num_leeches[1]}}, P: {{tracker.num_peers[1]}}, D: {{tracker.num_downloaded[1]}})
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
138
templates/qbittorrent/index.html
Normal file
138
templates/qbittorrent/index.html
Normal file
|
@ -0,0 +1,138 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% macro torrent_entry(torrent) %}
|
||||
{% set state_label,badge_type = status_map[torrent.state] or (torrent.state,'light') %}
|
||||
|
||||
<li class="list-group-item">
|
||||
<a href="{{url_for('qbittorrent_details',infohash=torrent.hash)}}">{{torrent.name|truncate(75)}}</a>
|
||||
(DL: {{torrent.dlspeed|filesizeformat(binary=true)}}/s, UL: {{torrent.upspeed|filesizeformat(binary=true)}}/s)
|
||||
<span class="badge badge-{{badge_type}}">{{state_label}}</span>
|
||||
{% if torrent.category %}
|
||||
<span class="badge badge-light">{{torrent.category}}</span>
|
||||
{% endif %}
|
||||
<div style="margin-top: 5px"></div>
|
||||
<div class="progress" style="width: 100%;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
style="width: {{(torrent.progress*100)|round(2)}}%;" role="progressbar"
|
||||
aria-valuenow="{{(torrent.progress*100)|round(2)}}" aria-valuemin="0" aria-valuemax="100">
|
||||
</div>
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated bg-primary"
|
||||
style="width: {{((([torrent.availability,1]|min)-torrent.progress)*100)|round(2)}}%;" role="progressbar"
|
||||
aria-valuenow="{{((([torrent.availability,1]|min)-torrent.progress)*100)|round(2)}}" aria-valuemin="0" aria-valuemax="100">
|
||||
</div>
|
||||
<small class="justify-content-center d-flex position-absolute w-100">{{(torrent.progress*100)|round(2)}} % (ETA: {{[torrent.eta,torrent.eta_act]|min|round(0)|timedelta(clamp=true)}})</small>
|
||||
</div>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<h2>
|
||||
<a href="{{config.APP_CONFIG.qbt_url}}">QBittorrent</a>
|
||||
{{qbt.version}}
|
||||
(DL: {{qbt.server_state.dl_info_speed|filesizeformat(binary=True)}}/s,
|
||||
UL: {{qbt.server_state.up_info_speed|filesizeformat(binary=True)}}/s)
|
||||
</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Total Uploaded
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.server_state.alltime_ul|filesizeformat(binary=True)}}
|
||||
</div>
|
||||
<div class="col">
|
||||
Total Downloaded
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.server_state.alltime_dl|filesizeformat(binary=True)}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Session Uploaded
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.server_state.up_info_data|filesizeformat(binary=True)}}
|
||||
</div>
|
||||
<div class="col">
|
||||
Session Downloaded
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.server_state.dl_info_data|filesizeformat(binary=True)}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Torrents
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.torrents|length}}
|
||||
</div>
|
||||
<div class="col">
|
||||
Total Queue Size
|
||||
</div>
|
||||
<div class="col">
|
||||
{{qbt.torrents.values()|map(attribute='size')|sum|filesizeformat(binary=true)}}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<form method="GET">
|
||||
<select class="form-control" name="sort" onchange="this.parentElement.submit()">
|
||||
<option value="">Sort by</option>
|
||||
{% for key,value in sort_by_choices.items() %}
|
||||
<option value="{{key}}">{{value}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for state,torrents in qbt.torrents.values()|sort(attribute='state')|groupby('state') %}
|
||||
{% set state_label,badge_type = status_map[state] or (state,'light') %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href={{url_for("qbittorrent",state=state)}} >{{state_label}}</a>
|
||||
</div>
|
||||
<div class="col">
|
||||
{{torrents|length}}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if state_filter %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href={{url_for("qbittorrent")}}>[Clear filter]</a>
|
||||
</div>
|
||||
<div class="col">
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<ul style="padding-bottom: 10px;" class="list-group">
|
||||
{% for torrent in qbt.torrents.values()|sort(attribute=sort_by,reverse=True) %}
|
||||
{% set state_label,badge_type = status_map[torrent.state] or (torrent.state,'light') %}
|
||||
{% if state_filter %}
|
||||
{% if torrent.state==state_filter %}
|
||||
{{torrent_entry(torrent)}}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{torrent_entry(torrent)}}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
0
templates/radarr/details.html
Normal file
0
templates/radarr/details.html
Normal file
28
templates/radarr/index.html
Normal file
28
templates/radarr/index.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'utils.html' import make_tabs %}
|
||||
|
||||
{% macro movie_list() %}
|
||||
{% for movie in movies|sort(attribute='sortTitle') %}
|
||||
<h6>
|
||||
<a href="{{urljoin(config.APP_CONFIG.radarr_url,'movie/'+movie.titleSlug)}}">{{movie.title}}</a>
|
||||
({{movie.year}})
|
||||
{% for genre in movie.genres %}
|
||||
<span class="badge badge-secondary">{{genre}}</span>
|
||||
{% endfor %}
|
||||
<span class="badge badge-info">{{movie.status|title}}</span>
|
||||
</h6>
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
{% block app_content %}
|
||||
<h2>
|
||||
<a href="{{config.APP_CONFIG.radarr_url}}">Radarr</a>
|
||||
v{{status.version}} ({{movies|count}} Movies)
|
||||
</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{movie_list()}}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
23
templates/remote/add.html
Normal file
23
templates/remote/add.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'utils.html' import custom_render_form_row,make_tabs %}
|
||||
{% from 'bootstrap/form.html' import render_form, render_field, render_form_row %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
{% if form %}
|
||||
<h1>Grant remote access</h1>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<form method="post" class="form">
|
||||
{{form.csrf_token()}}
|
||||
{{custom_render_form_row([form.name])}}
|
||||
{{custom_render_form_row([form.ssh_key])}}
|
||||
{{custom_render_form_row([form.add])}}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
78
templates/remote/index.html
Normal file
78
templates/remote/index.html
Normal file
|
@ -0,0 +1,78 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'utils.html' import custom_render_form_row,make_tabs %}
|
||||
{% from 'bootstrap/utils.html' import render_icon %}
|
||||
{% from 'bootstrap/form.html' import render_form, render_field, render_form_row %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<h1>
|
||||
Remote access <a href={{url_for("remote_add")}}>{{render_icon("person-plus-fill")}}</a>
|
||||
</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<h4>SSH</h4>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Type</th>
|
||||
<th>Key fingerprint</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
{% for key in ssh %}
|
||||
<tr {{ {"class":"text-muted" if key.disabled else none}|xmlattr }}>
|
||||
<td>
|
||||
{% if key.disabled %}
|
||||
<a href="{{url_for("remote",enabled=True,key=key.key)}}">{{render_icon("person-x-fill",color='danger')}}</a>
|
||||
{% else %}
|
||||
<a href="{{url_for("remote",enabled=False,key=key.key)}}">{{render_icon("person-check-fill",color='success')}}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{key.type}}</td>
|
||||
<td title="{{key.key}}">{{key.fingerprint}}</td>
|
||||
<td>{{key.name}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<h4><a href="{{cfg().jellyfin_url}}web/index.html#!/userprofiles.html">Jellyfin</a></h4>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Last Login</th>
|
||||
<th>Last Active</th>
|
||||
<th>Bandwidth Limit</th>
|
||||
</tr>
|
||||
{% for user in jf|sort(attribute="LastLoginDate",reverse=True) %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{cfg().jellyfin_url}}web/index.html#!/useredit.html?userId={{user.Id}}">
|
||||
{{user.Name}}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if "LastLoginDate" in user %}
|
||||
{{user.LastLoginDate|fromiso|ago_dt_utc(2)}} ago
|
||||
{% else %}
|
||||
Never
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if "LastActivityDate" in user %}
|
||||
{{user.LastActivityDate|fromiso|ago_dt_utc(2)}} ago
|
||||
{% else %}
|
||||
Never
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{user.Policy.RemoteClientBitrateLimit|filesizeformat(binary=False)}}/s</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
10
templates/search/details.html
Normal file
10
templates/search/details.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>{{info.title}} ({{info.year}})</h1>
|
||||
<h2>{{info.hasFile}}</h2>
|
||||
<p>{{info.id}}</p>
|
||||
<pre>
|
||||
{{info|tojson(indent=4)}}
|
||||
</pre>
|
||||
{% endblock %}
|
18
templates/search/include/movie.html
Normal file
18
templates/search/include/movie.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% macro movie_results(results) -%}
|
||||
<div class="d-flex flex-wrap">
|
||||
{%for result in results %}
|
||||
<form action="search/details" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="type" value="movie"/>
|
||||
<input type="hidden" name="data" value="{{result|tojson|urlencode}}"/>
|
||||
<a style="cursor: pointer" onclick="this.parentElement.submit()">
|
||||
{% for poster in result.images|selectattr('coverType','eq','poster') %}
|
||||
<img class="img-fluid poster" src="{{poster.url}}" title="{{result.title}}">
|
||||
{% else %}
|
||||
<img class="img-fluid poster" src="{{url_for('placeholder',width=333, height=500, text=result.title, wrap = 15)}}" title="{{result.title}}" />
|
||||
{% endfor %}
|
||||
</a>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
123
templates/search/include/torrent.html
Normal file
123
templates/search/include/torrent.html
Normal file
|
@ -0,0 +1,123 @@
|
|||
|
||||
{% macro torrent_result_row(result,with_tracker=false) -%}
|
||||
<tr>
|
||||
<td colspan="{{4 if with_tracker else 3}}">
|
||||
<hr/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="{{4 if with_tracker else 3}}">
|
||||
<input type="checkbox" id="torrent_selected" name="torrent[]" value="{{result.Link or result.MagnetUri}}">
|
||||
<a href="{{result.Link or result.MagnetUri}}">
|
||||
{{result.Title}}
|
||||
</a>
|
||||
{% if result.DownloadVolumeFactor==0.0 %}
|
||||
<span class="badge badge-success">Freeleech</span>
|
||||
{% endif %}
|
||||
{% if result.UploadVolumeFactor > 1.0 %}
|
||||
<span class="badge badge-success">UL x{{result.UploadVolumeFactor}}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{result.CategoryDesc}}
|
||||
</td>
|
||||
<td>
|
||||
{{result.Size|filesizeformat}}
|
||||
</td>
|
||||
{% if with_tracker %}
|
||||
<td>
|
||||
<a href="{{result.Guid}}">
|
||||
{{result.Tracker}}
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
({{result.Seeders}}/{{result.Peers}}/{{ "?" if result.Grabs is none else result.Grabs}})
|
||||
</td>
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro torrent_result_grouped(results) %}
|
||||
{% if results %}
|
||||
<table class="torrent_results">
|
||||
{% for tracker,results in results.Results|groupby(attribute="Tracker") %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<h2>{{tracker}} ({{results|length}})</h2>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="{{4 if with_tracker else 3}}">
|
||||
Name
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Category
|
||||
</th>
|
||||
<th>
|
||||
Size
|
||||
</th>
|
||||
<th>
|
||||
Seeds/Peers/Grabs
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{%for result in results|sort(attribute='Gain',reverse=true) %}
|
||||
{{ torrent_result_row(result,with_tracker=false) }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td colspan="{{4 if with_tracker else 3}}">
|
||||
<hr/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro torrent_results(results,group_by_tracker=false) %}
|
||||
<form action="/api/add_torrent" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="text" class="form-control" id="category" name="category" placeholder="Category"/>
|
||||
<span class="badge badge-primary" style="cursor: pointer" onclick="this.parentElement.submit()">
|
||||
Add selected to QBittorrent
|
||||
</span>
|
||||
{% if group_by_tracker %}
|
||||
{{ torrent_result_grouped(results) }}
|
||||
{% else %}
|
||||
{% if results %}
|
||||
<table class="torrent_results">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="{{4 if with_tracker else 3}}">
|
||||
Name
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Category
|
||||
</th>
|
||||
<th>
|
||||
Size
|
||||
</th>
|
||||
<th>
|
||||
Tracker
|
||||
</th>
|
||||
<th>
|
||||
Seeds/Peers/Grabs
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for result in results.Results|sort(attribute='Gain',reverse=true) %}
|
||||
{{ torrent_result_row(result,with_tracker=true) }}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endmacro %}
|
23
templates/search/include/tv_show.html
Normal file
23
templates/search/include/tv_show.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
|
||||
{% macro tv_show_results(results) -%}
|
||||
<div class="d-flex flex-wrap">
|
||||
{% for result in results %}
|
||||
<form action="search/details" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="type" value="show"/>
|
||||
<input type="hidden" name="data" value="{{result|tojson|urlencode}}" />
|
||||
<a style="cursor: pointer" onclick="this.parentElement.submit()">
|
||||
{% for banner in result.images|selectattr('coverType','eq','banner') %}
|
||||
<img class="img-fluid banner" src="{{client.sonarr.url.rsplit("/",2)[0]+banner.url}}" title="{{result.title}}" />
|
||||
{% else %}
|
||||
{% set poster=(result.images|selectattr('coverType','eq','poster')|first) %}
|
||||
{% if poster %}
|
||||
{% set poster=(client.sonarr.url.rsplit("/",2)[0]+poster.url) %}
|
||||
{% endif %}
|
||||
<img class="img-fluid banner" src="{{url_for('placeholder',width=758, height=140, poster=poster, text=result.title)}}" title="{{result.title}}" />
|
||||
{% endfor %}
|
||||
</a>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
64
templates/search/index.html
Normal file
64
templates/search/index.html
Normal file
|
@ -0,0 +1,64 @@
|
|||
{% extends "base.html" %}
|
||||
{% from "utils.html" import make_tabs, custom_render_form_row %}
|
||||
{% from 'bootstrap/form.html' import render_form, render_field, render_form_row %}
|
||||
{% from 'bootstrap/table.html' import render_table %}
|
||||
{% from 'search/include/tv_show.html' import tv_show_results with context %}
|
||||
{% from 'search/include/movie.html' import movie_results with context %}
|
||||
{% from 'search/include/torrent.html' import torrent_results with context %}
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<style>
|
||||
.poster {
|
||||
height: 500px;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
{% if form %}
|
||||
<h1>Search</h1>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
{% if session.new_torrents %}
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{% for torrent in session.pop('new_torrents',{}).values() %}
|
||||
<p>
|
||||
Added <a class="alert-link" href="{{url_for('qbittorrent',infohash=torrent.hash)}}">{{torrent.name}}</a>
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form %}
|
||||
<form method="post" class="form">
|
||||
{{form.csrf_token()}}
|
||||
{{custom_render_form_row([form.query],render_args={'form_type':'horizontal','horizontal_columns':('lg',1,11)})}}
|
||||
{{custom_render_form_row([form.tv_shows,form.movies,form.torrents])}}
|
||||
{{custom_render_form_row([form.group_by_tracker])}}
|
||||
{{custom_render_form_row([form.indexer])}}
|
||||
{{custom_render_form_row([form.search])}}
|
||||
</form>
|
||||
{% else %}
|
||||
<h1>Search results for '{{search_term}}'</h1>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% set search_results = [
|
||||
(results.tv_shows,"tv","TV Shows",tv_show_results,{}),
|
||||
(results.movies,"movie","Movies",movie_results,{}),
|
||||
(results.torrents,"torrent","Torrents",torrent_results,{"group_by_tracker":group_by_tracker}),
|
||||
] %}
|
||||
|
||||
{% if results %}
|
||||
{% set tabs = [] %}
|
||||
{% for results,id_name,label,func,kwargs in search_results if results %}
|
||||
{% do tabs.append((label,func(results,**kwargs))) %}
|
||||
{% endfor %}
|
||||
{{make_tabs(tabs)}}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
0
templates/sonarr/details.html
Normal file
0
templates/sonarr/details.html
Normal file
28
templates/sonarr/index.html
Normal file
28
templates/sonarr/index.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'utils.html' import make_tabs %}
|
||||
|
||||
{% macro series_list() %}
|
||||
{% for show in series|sort(attribute='sortTitle') %}
|
||||
<h6>
|
||||
<a href="{{urljoin(config.APP_CONFIG.sonarr_url,'series/'+show.titleSlug)}}">{{show.title}}</a>
|
||||
({{show.year}})
|
||||
{% for genre in show.genres %}
|
||||
<span class="badge badge-secondary">{{genre}}</span>
|
||||
{% endfor %}
|
||||
<span class="badge badge-info">{{show.status|title}}</span>
|
||||
</h6>
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
{% block app_content %}
|
||||
<h2>
|
||||
<a href="{{config.APP_CONFIG.sonarr_url}}">Sonarr</a>
|
||||
v{{status.version}} ({{series|count}} Shows)
|
||||
</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{series_list()}}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
43
templates/test.html
Normal file
43
templates/test.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
|
||||
{% block scripts %}
|
||||
{{super()}}
|
||||
<script lang="text/javascript">
|
||||
let prog=0;
|
||||
function setPrograb(selector,value) {
|
||||
let rv=Math.round(prog*100,2)/100;
|
||||
$(selector).attr('style','width: '+rv+"%;");
|
||||
$(selector).attr('aria-valuenow',rv);
|
||||
$(selector).text(rv+' %');
|
||||
}
|
||||
setInterval(() => {
|
||||
for (var i=0;i<100;++i) {
|
||||
prog=Math.random()*100;
|
||||
setPrograb("#prog_test_bar_"+i,prog);
|
||||
}
|
||||
},1000)
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block app_content__ %}
|
||||
<div class="row">
|
||||
{{render_form(form)}}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block app_content %}
|
||||
{% for i in range(100) %}
|
||||
<div class="row">
|
||||
<div id="prog_test_{{i}}" class="progress" style="width: 100%;">
|
||||
<div id="prog_test_bar_{{i}}" class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
style="width: 0%;" role="progressbar"
|
||||
aria-valuenow="" aria-valuemin="0" aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
30
templates/transcode/profiles.html
Normal file
30
templates/transcode/profiles.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'utils.html' import make_tabs %}
|
||||
{% from 'bootstrap/form.html' import render_form, render_field, render_form_row %}
|
||||
|
||||
{% macro profile_list() %}
|
||||
{% for name, cfg in config.APP_CONFIG.transcode_profiles.items() %}
|
||||
<h3>{{name}}</h3>
|
||||
<h5>{{cfg.doc}}</h5>
|
||||
<pre>ffmpeg -i <infile> {{cfg.command}} <outfile></pre>
|
||||
{% if cfg.vars %}
|
||||
{% for var,doc in cfg.vars.items() %}
|
||||
<p>
|
||||
<pre class="inline">{{var}}</pre>
|
||||
({{doc}}{% if cfg.defaults[var] %}, Default: <pre class="inline">{{cfg.defaults[var]}}</pre>{% endif %})</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<hr>
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>Transcode profiles</h1>
|
||||
{{profile_list()}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
85
templates/utils.html
Normal file
85
templates/utils.html
Normal file
|
@ -0,0 +1,85 @@
|
|||
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
|
||||
{% macro custom_render_form_row(fields, row_class='form-row', col_class_default='col', col_map={}, button_map={}, button_style='', button_size='', render_args={}) %}
|
||||
<div class="{{ row_class }}">
|
||||
{% for field in fields %}
|
||||
{% if field.name in col_map %}
|
||||
{% set col_class = col_map[field.name] %}
|
||||
{% else %}
|
||||
{% set col_class = col_class_default %}
|
||||
{% endif %}
|
||||
<div class="{{ col_class }}">
|
||||
{{ render_field(field, button_map=button_map, button_style=button_style, button_size=button_size, **render_args) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro make_tabs(tabs)%}
|
||||
{% set tabs_id = tabs|tojson|hash %}
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">
|
||||
{% for label,tab in tabs if tab %}
|
||||
{% set id_name = [loop.index,tabs_id ]|join("-") %}
|
||||
{% if not (loop.first and loop.last) %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{'active' if loop.first}}" id="nav-{{id_name}}-tab" data-toggle="pill" href="#pills-{{id_name}}" role="tab" aria-controls="pills-{{id_name}}" aria-selected="{{loop.first}}">
|
||||
{{label}}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<div class="tab-content" id="searchResults">
|
||||
{% for label,tab in tabs if tab %}
|
||||
{% set id_name = [loop.index,tabs_id ]|join("-") %}
|
||||
<div class="tab-pane fade {{'show active' if loop.first}}" id="pills-{{id_name}}" role="tabpanel" aria-labelledby="nav-{{id_name}}-tab">
|
||||
{{ tab|safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_tree(tree) -%}
|
||||
<ul class="file_tree">
|
||||
{% for node,children in tree.items() recursive %}
|
||||
{% if node=="__info__" or not children is mapping -%}
|
||||
{% set file = children %}
|
||||
<li>
|
||||
<div class="row" style="margin-left: 10px;">
|
||||
<div class="progress" style="width: 100%;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
style="width: {{(file.progress*100)|round(2)}}%;" role="progressbar"
|
||||
aria-valuenow="{{(file.progress*100)|round(2)}}" aria-valuemin="0" aria-valuemax="100">
|
||||
{{(file.progress*100)|round(2)}} % ({{file.size|filesizeformat(binary=True)}})
|
||||
</div>
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated bg-primary"
|
||||
style="width: {{(((file.availability or 1) - file.progress)*100)|round(2)}}%;" role="progressbar"
|
||||
aria-valuenow="{{(((file.availability or 1) - file.progress)*100)|round(2)}}" aria-valuemin="0" aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% else -%}
|
||||
<li class="tree">
|
||||
<span class="{{'custom_caret' if children.items() else '' }}">
|
||||
{{node}}
|
||||
</span>
|
||||
{% if children.items() -%}
|
||||
<ul class="tree nested">
|
||||
{{loop(children.items())}}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endmacro %}
|
Loading…
Add table
Add a link
Reference in a new issue