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