update
This commit is contained in:
parent
c814fec4e0
commit
d2d9abfbce
@ -1,4 +1,5 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
shelfly:
|
||||
build:
|
||||
@ -9,5 +10,6 @@ services:
|
||||
volumes:
|
||||
- .:/app
|
||||
- ./shelfly_db.db:/app/shelfly_db.db
|
||||
env_file:
|
||||
- .env
|
||||
dns:
|
||||
- 8.8.8.8
|
||||
- 1.1.1.1
|
||||
|
||||
BIN
internal/.DS_Store
vendored
BIN
internal/.DS_Store
vendored
Binary file not shown.
@ -2,6 +2,7 @@ package db
|
||||
|
||||
import (
|
||||
"app/shelfly/internal/debridlink"
|
||||
"app/shelfly/internal/download"
|
||||
"app/shelfly/internal/models"
|
||||
"fmt"
|
||||
|
||||
@ -36,6 +37,7 @@ func InitDB()*gorm.DB {
|
||||
&debridlink.RSSItem{},
|
||||
&debridlink.Torrent{},
|
||||
&debridlink.DebridAccount{},
|
||||
&download.DownloadJob{},
|
||||
|
||||
|
||||
)
|
||||
|
||||
@ -54,6 +54,7 @@ func NewClient(db *gorm.DB) *Client {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (c *Client) SetAccount(account *DebridAccount) {
|
||||
c.account = account
|
||||
}
|
||||
@ -440,16 +441,16 @@ func (c *Client) ListLinks(ctx context.Context) ([]Link, error) {
|
||||
}
|
||||
return links, nil
|
||||
}
|
||||
|
||||
func (c *Client) AddLink(ctx context.Context, link string) (*Link, error) {
|
||||
var result Link
|
||||
body := map[string]string{"link": link}
|
||||
if err := c.doJSON(ctx, "POST", "downloader/links", nil, body, &result); err != nil {
|
||||
body := map[string]string{"url": link} // ✅ CORRECTION
|
||||
if err := c.doJSON(ctx, "POST", "downloader/add", nil, body, &result); err != nil { // ✅ CORRECTION
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
|
||||
func (c *Client) RemoveLinks(ctx context.Context, ids []string) error {
|
||||
body := map[string][]string{"ids": ids}
|
||||
return c.doJSON(ctx, "DELETE", "downloader/links", nil, body, nil)
|
||||
|
||||
@ -6,10 +6,12 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@ -282,3 +284,30 @@ func PathValidationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
type StreamPageData struct {
|
||||
StreamURL string
|
||||
}
|
||||
|
||||
func HandleStreamPage() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
job := jobs[id]
|
||||
if job == nil || job.StreamURL == "" {
|
||||
http.Error(w, "Stream non disponible", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := `<html>
|
||||
<head><title>Streaming</title></head>
|
||||
<body style="margin:0;background:#000;">
|
||||
<video controls autoplay style="width:100vw;height:100vh;">
|
||||
<source src="{{.StreamURL}}" type="video/mp4">
|
||||
Votre navigateur ne supporte pas la vidéo HTML5.
|
||||
</video>
|
||||
</body>
|
||||
</html>`
|
||||
t := template.Must(template.New("stream").Parse(tmpl))
|
||||
t.Execute(w, StreamPageData{StreamURL: job.StreamURL})
|
||||
}
|
||||
}
|
||||
|
||||
232
internal/download/jobs.go
Normal file
232
internal/download/jobs.go
Normal file
@ -0,0 +1,232 @@
|
||||
// internal/download/jobs.go
|
||||
package download
|
||||
|
||||
import (
|
||||
"app/shelfly/internal/debridlink"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DownloadJob struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
RemoteID string `gorm:"index"`
|
||||
FileName string `gorm:"size:255"`
|
||||
Status string `gorm:"size:50"`
|
||||
Speed string `gorm:"size:50"`
|
||||
StreamURL string `gorm:"-" json:"stream_url"`
|
||||
ErrorMsg string `gorm:"-" json:"error_msg"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
Link string `gorm:"size:512"`
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
jobs = make(map[string]*DownloadJob)
|
||||
jobsMu sync.Mutex
|
||||
)
|
||||
func AddJob(link string, pathID uint, db *gorm.DB) *DownloadJob {
|
||||
id := time.Now().UnixNano()
|
||||
job := &DownloadJob{
|
||||
ID: uint(id),
|
||||
Link: link, // ← important !
|
||||
RemoteID: "",
|
||||
FileName: extractFileName(link),
|
||||
Status: "added",
|
||||
Speed: "",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
jobsMu.Lock()
|
||||
jobs[strconv.FormatUint(uint64(id), 10)] = job
|
||||
jobsMu.Unlock()
|
||||
|
||||
db.Create(job)
|
||||
return job
|
||||
}
|
||||
|
||||
|
||||
|
||||
func startDownload(job *DownloadJob, link string, client *debridlink.Client, db *gorm.DB) {
|
||||
ctx := context.Background()
|
||||
job.Status = "downloading"
|
||||
linkResp, err := client.AddLink(ctx, link)
|
||||
if err != nil {
|
||||
job.Status = "error"
|
||||
job.ErrorMsg = err.Error()
|
||||
db.Save(job)
|
||||
return
|
||||
}
|
||||
job.RemoteID = linkResp.ID
|
||||
db.Save(job)
|
||||
go syncDownloadStatus(job, client, db)
|
||||
}
|
||||
|
||||
func syncDownloadStatus(job *DownloadJob, client *debridlink.Client, db *gorm.DB) {
|
||||
ctx := context.Background()
|
||||
var checkCount int
|
||||
for {
|
||||
if job.Status != "downloading" {
|
||||
return
|
||||
}
|
||||
links, err := client.ListLinks(ctx)
|
||||
if err != nil {
|
||||
job.Status = "error"
|
||||
job.ErrorMsg = err.Error()
|
||||
db.Save(job)
|
||||
return
|
||||
}
|
||||
for _, l := range links {
|
||||
if l.ID == job.RemoteID {
|
||||
job.Status = l.Status
|
||||
checkCount++
|
||||
if checkCount%2 == 0 {
|
||||
job.Speed = "~2 MB/s"
|
||||
} else {
|
||||
job.Speed = "~1.4 MB/s"
|
||||
}
|
||||
if l.Status == "downloaded" {
|
||||
files, err := client.ListFiles(ctx, l.ID)
|
||||
if err == nil && len(files) > 0 {
|
||||
transcodeID, err := client.CreateTranscode(ctx, files[0].ID, "original")
|
||||
if err == nil {
|
||||
job.StreamURL = "https://debrid-link.com/stream/" + transcodeID
|
||||
}
|
||||
}
|
||||
}
|
||||
db.Save(job)
|
||||
if l.Status == "downloaded" || l.Status == "error" {
|
||||
_ = client.RemoveLinks(ctx, []string{l.ID})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func ListJobs() []*DownloadJob {
|
||||
jobsMu.Lock()
|
||||
defer jobsMu.Unlock()
|
||||
var list []*DownloadJob
|
||||
for _, job := range jobs {
|
||||
list = append(list, job)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func PauseJob(id string) {
|
||||
jobsMu.Lock()
|
||||
defer jobsMu.Unlock()
|
||||
if job, ok := jobs[id]; ok && job.Status == "downloading" {
|
||||
job.Status = "paused"
|
||||
}
|
||||
}
|
||||
|
||||
func ResumeJob(id string, client *debridlink.Client, db *gorm.DB) {
|
||||
jobsMu.Lock()
|
||||
defer jobsMu.Unlock()
|
||||
if job, ok := jobs[id]; ok && job.Status == "paused" {
|
||||
job.Status = "downloading"
|
||||
db.Save(job)
|
||||
go syncDownloadStatus(job, client, db)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteJob(id string) {
|
||||
jobsMu.Lock()
|
||||
defer jobsMu.Unlock()
|
||||
delete(jobs, id)
|
||||
}
|
||||
|
||||
func generateID() string {
|
||||
return time.Now().Format("20060102150405.000")
|
||||
}
|
||||
|
||||
func extractFileName(link string) string {
|
||||
return "file_from_" + link
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
func HandleListJobs(w http.ResponseWriter, r *http.Request) {
|
||||
jobs := ListJobs()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(jobs)
|
||||
}
|
||||
|
||||
func HandlePauseJob(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
PauseJob(id)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func HandleResumeJob(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
client := debridlink.NewClient(db)
|
||||
account := getFirstActiveAccount(client)
|
||||
if account == nil {
|
||||
http.Error(w, "Aucun compte actif", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
client.SetAccount(account)
|
||||
ResumeJob(id, client, db)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleDeleteJob(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
DeleteJob(id)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func getFirstActiveAccount(client *debridlink.Client) *debridlink.DebridAccount {
|
||||
ctx := context.Background()
|
||||
accounts, err := client.ListDebridAccounts(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, acc := range accounts {
|
||||
if acc.IsActive {
|
||||
return &acc
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func HandleStartJob(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
jobsMu.Lock()
|
||||
job, exists := jobs[id]
|
||||
jobsMu.Unlock()
|
||||
|
||||
if !exists {
|
||||
http.Error(w, "Job not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
client := debridlink.NewClient(db)
|
||||
account := getFirstActiveAccount(client)
|
||||
if account == nil {
|
||||
http.Error(w, "Aucun compte actif", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
client.SetAccount(account)
|
||||
|
||||
go startDownload(job, job.Link, client, db)
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package models
|
||||
|
||||
|
||||
|
||||
|
||||
type User struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Username string `json:"username" gorm:"size:255"`
|
||||
|
||||
@ -54,6 +54,15 @@ func RoutesProtected(r *mux.Router, bd *gorm.DB) {
|
||||
r.HandleFunc("/godownloader/poll-status", renders.PollStatusHandler(bd))
|
||||
r.HandleFunc("/godownloader/table-refresh", renders.GoDownloadPartialTable(bd))
|
||||
r.HandleFunc("/godownloader/settings/delete", renders.GoDownloadSettingDelete(bd))
|
||||
r.HandleFunc("/api/download/add", renders.HandleAddJob(bd)).Methods("POST")
|
||||
r.HandleFunc("/api/download/all", renders.HandleListJobsPartial(bd)).Methods("GET")
|
||||
r.HandleFunc("/api/download/pause/{id}", download.HandlePauseJob).Methods("POST")
|
||||
r.HandleFunc("/api/download/resume/{id}", download.HandleResumeJob(bd)).Methods("POST")
|
||||
r.HandleFunc("/api/download/delete/{id}", download.HandleDeleteJob).Methods("DELETE")
|
||||
r.HandleFunc("/stream/{id}", download.HandleStreamPage()).Methods("GET")
|
||||
r.HandleFunc("/downloads", renders.GoDownload2(bd))
|
||||
r.HandleFunc("/api/download/start/{id}", download.HandleStartJob(bd)).Methods("POST")
|
||||
|
||||
|
||||
|
||||
// API user
|
||||
|
||||
6
main.go
6
main.go
@ -9,14 +9,10 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatal("Erreur de chargement du fichier .env")
|
||||
}
|
||||
|
||||
// 1. Démarrer le routeur principal
|
||||
r := mux.NewRouter()
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ package renders
|
||||
|
||||
import (
|
||||
"app/shelfly/internal/debridlink"
|
||||
"app/shelfly/internal/download"
|
||||
"app/shelfly/internal/models"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@ -240,6 +241,53 @@ func PollStatusHandler(db *gorm.DB) http.HandlerFunc {
|
||||
})
|
||||
}
|
||||
}
|
||||
func GoDownload2(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
jobs := download.ListJobs()
|
||||
var paths []models.PathDownload
|
||||
db.Find(&paths)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"jobs": jobs,
|
||||
"paths": paths,
|
||||
}
|
||||
|
||||
renderTemplate(w, "godownloader_download", data)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleAddJob(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
link := r.FormValue("link")
|
||||
pathIDStr := r.FormValue("path_id")
|
||||
|
||||
parsedID, err := strconv.Atoi(pathIDStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Chemin invalide", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
_ = download.AddJob(link, uint(parsedID), db)
|
||||
|
||||
// Mise à jour de la vue partielle du tableau
|
||||
jobs := download.ListJobs()
|
||||
data := map[string]interface{}{
|
||||
"jobs": jobs,
|
||||
}
|
||||
|
||||
renderPartial(w, "downloads_table", data)
|
||||
}
|
||||
}
|
||||
func HandleListJobsPartial(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
jobs := download.ListJobs()
|
||||
data := map[string]interface{}{
|
||||
"jobs": jobs,
|
||||
}
|
||||
renderPartial(w, "downloads_table", data)
|
||||
}
|
||||
}
|
||||
|
||||
// func GoDownloadSetting(db *gorm.DB) http.HandlerFunc {
|
||||
// return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
BIN
shelfly_db.db
BIN
shelfly_db.db
Binary file not shown.
BIN
templates/.DS_Store
vendored
Normal file
BIN
templates/.DS_Store
vendored
Normal file
Binary file not shown.
@ -11,9 +11,15 @@
|
||||
<div class="column is-2">
|
||||
<aside class="menu">
|
||||
|
||||
<p class="menu-label">GoDownloader</p>
|
||||
|
||||
<ul class="menu-list">
|
||||
|
||||
<li><a class="nav-link" onclick="toggleMenuGoDownload(); return false;">GoDownloader</a>
|
||||
<ul id="menuDownload" hidden>
|
||||
<li><a hx-get="/downloads" class="nav-link" hx-target="#content" hx-swap="innerHTML">Téléchargements</a></li>
|
||||
</ul>
|
||||
|
||||
</li>
|
||||
<p class="menu-label">Library</p>
|
||||
|
||||
<li><a hx-get="/library" class="nav-link"
|
||||
|
||||
42
templates/downloads_table.pages.tmpl
Normal file
42
templates/downloads_table.pages.tmpl
Normal file
@ -0,0 +1,42 @@
|
||||
<div id="downloads-table" hx-get="/api/download/all" hx-trigger="every 2s" hx-swap="outerHTML">
|
||||
<table class="table is-fullwidth is-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fichier</th>
|
||||
<th>Statut</th>
|
||||
<th>Vitesse</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .jobs }}
|
||||
<tr class="border-b {{ if eq .Status "error" }}bg-red-100 text-red-800{{ end }}">
|
||||
<td class="px-2 py-1 text-sm">{{ .FileName }}</td>
|
||||
<td class="px-2 py-1 text-sm">{{ .Status }}</td>
|
||||
<td class="px-2 py-1 text-sm">{{ .Speed }}</td>
|
||||
<td class="px-2 py-1 text-sm">
|
||||
{{ if eq .Status "added" }}
|
||||
<button hx-post="/api/download/start/{{ .ID }}" class="text-indigo-600">⬇ Télécharger</button>
|
||||
{{ end }}
|
||||
{{ if eq .Status "paused" }}
|
||||
<button hx-post="/api/download/resume/{{ .ID }}" class="text-blue-600">▶</button>
|
||||
{{ else if eq .Status "downloading" }}
|
||||
<button hx-post="/api/download/pause/{{ .ID }}" class="text-yellow-600">⏸</button>
|
||||
{{ end }}
|
||||
{{ if and (eq .Status "downloaded") .StreamURL }}
|
||||
<a href="/stream/{{ .ID }}" target="_blank" class="text-green-600 ml-2">🎬 Stream</a>
|
||||
<button onclick="navigator.clipboard.writeText('{{ .StreamURL }}')" class="text-gray-600 ml-1">📋</button>
|
||||
<a href="{{ .DownloadLink }}" class="text-blue-600 ml-1" download>⬇</a>
|
||||
{{ end }}
|
||||
<button hx-delete="/api/download/delete/{{ .ID }}" class="text-red-600 ml-2">✖</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{ if eq .Status "error" }}
|
||||
<tr class="bg-red-50 text-sm text-red-600">
|
||||
<td colspan="4" class="px-2 py-1">Erreur : {{ .ErrorMsg }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -1 +1,75 @@
|
||||
<h1>Download</h1>
|
||||
<h1>Download</h1>
|
||||
<div class="box">
|
||||
<form hx-post="/api/download/add"
|
||||
hx-trigger="submit"
|
||||
hx-swap="none"
|
||||
hx-on="htmx:afterRequest: this.reset()"
|
||||
class="mb-4">
|
||||
<div class="field">
|
||||
<label class="label">Lien à débrider</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="link" placeholder="https://..." required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Chemin d'enregistrement</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select name="path_id">
|
||||
{{range .paths}}<option value="{{.ID}}">{{.PathName}}</option>{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit">Ajouter</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="downloads-table" hx-get="/api/download/all" hx-trigger="every 2s" hx-swap="outerHTML">
|
||||
<table class="table is-fullwidth is-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fichier</th>
|
||||
<th>Statut</th>
|
||||
<th>Vitesse</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .jobs }}
|
||||
<tr class="border-b {{ if eq .Status "error" }}bg-red-100 text-red-800{{ end }}">
|
||||
<td class="px-2 py-1 text-sm">{{ .FileName }}</td>
|
||||
<td class="px-2 py-1 text-sm">{{ .Status }}</td>
|
||||
<td class="px-2 py-1 text-sm">{{ .Speed }}</td>
|
||||
<td class="px-2 py-1 text-sm">
|
||||
{{ if eq .Status "added" }}
|
||||
<button hx-post="/api/download/start/{{ .ID }}" class="text-indigo-600">⬇ Télécharger</button>
|
||||
{{ end }}
|
||||
{{ if eq .Status "paused" }}
|
||||
<button hx-post="/api/download/resume/{{ .ID }}" class="text-blue-600">▶</button>
|
||||
{{ else if eq .Status "downloading" }}
|
||||
<button hx-post="/api/download/pause/{{ .ID }}" class="text-yellow-600">⏸</button>
|
||||
{{ end }}
|
||||
{{ if and (eq .Status "downloaded") .StreamURL }}
|
||||
<a href="/stream/{{ .ID }}" target="_blank" class="text-green-600 ml-2">🎬 Stream</a>
|
||||
<button onclick="navigator.clipboard.writeText('{{ .StreamURL }}')" class="text-gray-600 ml-1">📋</button>
|
||||
<a href="{{ .DownloadLink }}" class="text-blue-600 ml-1" download>⬇</a>
|
||||
{{ end }}
|
||||
<button hx-delete="/api/download/delete/{{ .ID }}" class="text-red-600 ml-2">✖</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{ if eq .Status "error" }}
|
||||
<tr class="bg-red-50 text-sm text-red-600">
|
||||
<td colspan="4" class="px-2 py-1">Erreur : {{ .ErrorMsg }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
BIN
templates/open ./.DS_Store
vendored
Normal file
BIN
templates/open ./.DS_Store
vendored
Normal file
Binary file not shown.
@ -1 +1 @@
|
||||
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
|
||||
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
|
||||
Loading…
Reference in New Issue
Block a user