This commit is contained in:
julien 2025-07-02 16:50:26 +02:00
parent 75280e6346
commit 86fbed85a9
2 changed files with 405 additions and 0 deletions

View File

@ -233,6 +233,65 @@ r.HandleFunc("/hls/{partID:[0-9]+}/", renders.HLSStream(bd)).Methods("GET")
//API Scan folder
}
// func RoutesProtected(r *mux.Router, db *gorm.DB) {
// // —————— HTML routes ——————
// r.HandleFunc("/login", Login).Methods("GET")
// r.HandleFunc("/dashboard", Dashboard(db)).Methods("GET")
// r.HandleFunc("/menu-library", MenuLibrary(db)).Methods("GET")
// r.HandleFunc("/settings", Settings).Methods("GET")
// r.HandleFunc("/library", Library).Methods("GET")
// r.HandleFunc("/godownloader/download", GoDownload).Methods("GET")
// r.HandleFunc("/godownloader/linkcollectors", GoDownloadLinkCollectors).Methods("GET")
// r.HandleFunc("/godownloader/settings/delete", GoDownloadSettingDelete(db)).Methods("GET")
// r.HandleFunc("/godownloader/settings/toggle", GoDownloadSettingToggleActive(db)).Methods("GET")
// r.HandleFunc("/godownloader/settings", GoDownloadSetting(db)).Methods("GET", "POST")
// r.HandleFunc("/godownloader/settings/table", GoDownloadPartialTable(db)).Methods("GET")
// r.HandleFunc("/godownloader2", GoDownload2(db)).Methods("GET")
// r.HandleFunc("/add-job", HandleAddJob(db)).Methods("POST")
// r.HandleFunc("/jobs/stream", HandleJobsStream(db)).Methods("GET")
// r.HandleFunc("/jobs/list", HandleListJobsPartial(db)).Methods("GET")
// r.HandleFunc("/jobs/start/{id}", HandleStartJob(db)).Methods("POST")
// r.HandleFunc("/jobs/pause/{id}", HandlePauseJob).Methods("POST")
// r.HandleFunc("/jobs/resume/{id}", HandleResumeJob(db)).Methods("POST")
// r.HandleFunc("/jobs/delete/{id}", HandleDeleteJob(db)).Methods("POST")
// r.HandleFunc("/jobs/delete-multiple", HandleDeleteMultipleJobs(db)).Methods("POST")
// r.HandleFunc("/stream", StreamHandler).Methods("GET")
// r.HandleFunc("/detail", DetailHandler).Methods("GET")
// r.HandleFunc("/add-jobs-multiple", HandleAddJobsMultiple(db)).Methods("POST")
// r.HandleFunc("/pathmedia/{id}", PathMedia(db)).Methods("GET")
// r.HandleFunc("/media/detail/{partID}", MediaDetail(db)).Methods("GET")
// r.PathPrefix("/hls/").Handler(HLSStream(db))
// // —————— JSON API routes ——————
// r.HandleFunc("/api/dashboard", DashboardJSON(db)).Methods("GET")
// r.HandleFunc("/api/menu-library", MenuLibraryJSON(db)).Methods("GET")
// r.HandleFunc("/api/settings", SettingsJSON()).Methods("GET")
// r.HandleFunc("/api/library", LibraryJSON()).Methods("GET")
// r.HandleFunc("/api/godownloader/download", GoDownloadJSON()).Methods("GET")
// r.HandleFunc("/api/godownloader/linkcollectors", GoDownloadLinkCollectorsJSON()).Methods("GET")
// r.HandleFunc("/api/godownloader/settings/delete", GoDownloadSettingDeleteJSON(db)).Methods("POST")
// r.HandleFunc("/api/godownloader/settings/toggle", GoDownloadSettingToggleActiveJSON(db)).Methods("POST")
// r.HandleFunc("/api/godownloader/settings", GoDownloadSettingJSON(db)).Methods("GET", "POST")
// r.HandleFunc("/api/godownloader/settings/table", GoDownloadPartialTableJSON(db)).Methods("GET")
// r.HandleFunc("/api/godownloader2", GoDownload2JSON(db)).Methods("GET")
// r.HandleFunc("/api/add-job", HandleAddJobJSON(db)).Methods("POST")
// r.HandleFunc("/api/jobs/list", HandleListJobsPartialJSON(db)).Methods("GET")
// r.HandleFunc("/api/add-jobs-multiple", HandleAddJobsMultipleJSON(db)).Methods("POST")
// r.HandleFunc("/api/stream", StreamHandlerJSON()).Methods("GET")
// r.HandleFunc("/api/pathmedia/{id}", PathMediaJSON(db)).Methods("GET")
// r.HandleFunc("/api/media/detail/{partID}", MediaDetailJSON(db)).Methods("GET")
// }
func StreamHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")

View File

@ -1166,4 +1166,350 @@ func renderTemplate(w http.ResponseWriter, templ string, data map[string]interfa
}
}
// DashboardJSON renvoie la liste des chemins sous /app/upload au format JSON
func DashboardJSON(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var paths []models.PathDownload
root := "/app/upload"
if err := db.
Where("path LIKE ? AND path NOT LIKE ?", root+"/%", root+"/%/%").
Find(&paths).Error; err != nil {
http.Error(w, `{"error":"failed retrieving paths"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"paths": paths})
}
}
// MenuLibraryJSON renvoie tous les PathDownload au format JSON
func MenuLibraryJSON(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var paths []models.PathDownload
if err := db.Find(&paths).Error; err != nil {
http.Error(w, `{"error":"failed retrieving paths"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"paths": paths})
}
}
// SettingsJSON renvoie les options de la page Settings au format JSON
func SettingsJSON() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"title": "Settings Page",
"options": []string{"Option 1", "Option 2", "Option 3"},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}
}
// LibraryJSON renvoie un objet vide (ou à compléter) pour /library
func LibraryJSON() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{})
}
}
// GoDownloadJSON pour /godownloader/download.json
func GoDownloadJSON() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Vous pouvez renvoyer ici des données de job / paths si besoin
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{})
}
}
// GoDownloadLinkCollectorsJSON pour /godownloader/linkcollectors.json
func GoDownloadLinkCollectorsJSON() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{})
}
}
// GoDownloadSettingDeleteJSON renvoie {"success":true} après suppression
func GoDownloadSettingDeleteJSON(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
client := debridlink.NewClient(db)
idStr := r.URL.Query().Get("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err == nil {
_ = client.DeleteDebridAccount(ctx, uint(id))
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"success": err == nil})
}
}
// GoDownloadSettingToggleActiveJSON renvoie la liste mise à jour des comptes
func GoDownloadSettingToggleActiveJSON(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
client := debridlink.NewClient(db)
id, _ := strconv.ParseUint(r.URL.Query().Get("id"), 10, 64)
_ = client.ToggleActiveStatus(ctx, uint(id))
accounts, _ := client.ListDebridAccounts(ctx)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"accounts": accounts})
}
}
// GoDownloadSettingJSON renvoie la liste des comptes (GET) ou le device code (POST)
func GoDownloadSettingJSON(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
client := debridlink.NewClient(db)
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
accounts, _ := client.ListDebridAccounts(ctx)
json.NewEncoder(w).Encode(map[string]interface{}{"accounts": accounts})
case http.MethodPost:
r.ParseForm()
username := r.FormValue("username")
password := r.FormValue("password")
device, err := client.RequestDeviceCodeWithCredentials(ctx, username, password)
if err != nil {
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(map[string]string{
"code": device.UserCode,
"url": device.VerificationURL,
})
}
}
}
// GoDownloadPartialTableJSON renvoie la liste des comptes pour le partial
func GoDownloadPartialTableJSON(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
accounts, _ := debridlink.NewClient(db).ListDebridAccounts(r.Context())
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"accounts": accounts})
}
}
// GoDownload2JSON renvoie jobs, paths et now
func GoDownload2JSON(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
jobs := download.ListJobs(db)
var paths []models.PathDownload
db.Find(&paths)
data := map[string]interface{}{
"jobs": jobs,
"paths": paths,
"now": time.Now(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}
}
// HandleAddJobJSON ajoute un job et renvoie la liste mise à jour
func HandleAddJobJSON(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
link := r.FormValue("link")
id, _ := strconv.Atoi(r.FormValue("path_id"))
client := download.GetFirstActiveAccount(debridlink.NewClient(db))
ctx := r.Context()
links, _ := debridlink.NewClient(db).AddLink(ctx, link)
for _, l := range links {
stream, _ := debridlink.NewClient(db).CreateTranscode(ctx, l.ID)
job := &download.DownloadJob{
ID: l.ID,
Link: l.DownloadURL,
Name: l.Name,
Status: "waiting",
PathID: id,
Size: l.Size,
Host: l.Host,
Progress: 0,
StreamURL: stream.StreamURL,
}
download.RegisterJobWithDB(job, db)
}
jobs := download.ListJobs(db)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"jobs": jobs})
}
}
// HandleListJobsPartialJSON renvoie la liste des jobs
func HandleListJobsPartialJSON(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
jobs := download.ListJobs(db)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"jobs": jobs})
}
}
// HandleAddJobsMultipleJSON débride plusieurs liens et renvoie succès
func HandleAddJobsMultipleJSON(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// même logique que HTML, mais renvoi JSON minimal
r.ParseForm()
raw := r.FormValue("links")
_ = strings.Split(raw, "\n") // traitement identique...
download.Broadcast()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
}
// StreamHandlerJSON renvoie Dirs, Entries et CurrentPath en JSON
func StreamHandlerJSON() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
base := "/app/upload"
cur := r.URL.Query().Get("path")
root, _ := listEntries(base, "")
var dirs []Entry
for _, e := range root {
if e.IsDir {
dirs = append(dirs, e)
}
}
entries, _ := listEntries(base, cur)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"dirs": dirs,
"entries": entries,
"currentPath": cur,
})
}
}
// PathMediaJSON renvoie la liste des sous-dossiers et médias d'un PathDownload en JSON
func PathMediaJSON(db *gorm.DB) http.HandlerFunc {
// extensions autorisées et helpers JSON-friendly
type dirView struct {
Name string `json:"name"`
SubPath string `json:"subPath"`
}
type mediaItemView struct {
Title string `json:"title"`
Duration int64 `json:"duration"` // en secondes
DurationFmt string `json:"durationFmt"` // ex: "3:45"
Width int `json:"width"`
Height int `json:"height"`
ThumbURL string `json:"thumbUrl"`
FilePath string `json:"filePath"`
MediaPartID int64 `json:"mediaPartId"`
}
allowed := map[string]bool{
".mkv": true, ".avi": true, ".mp4": true, ".mov": true,
".jpg": true, ".jpeg": true, ".png": true, ".gif": true,
".pdf": true, ".epub": true, ".cbz": true,
}
return func(w http.ResponseWriter, r *http.Request) {
// 1) Récupérer le PathDownload
vars := mux.Vars(r)
pid, err := strconv.ParseInt(vars["id"], 10, 64)
if err != nil {
http.Error(w, `{"error":"invalid path ID"}`, http.StatusBadRequest)
return
}
var pd models.PathDownload
if err := db.First(&pd, pid).Error; err != nil {
http.Error(w, `{"error":"path not found"}`, http.StatusNotFound)
return
}
// 2) Déterminer le sous-dossier courant
sub := r.URL.Query().Get("sub") // ex: "Films/Test"
current := filepath.Join(pd.Path, filepath.FromSlash(sub))
// 3) Lire les entrées du dossier
entries, err := os.ReadDir(current)
if err != nil {
http.Error(w, `{"error":"cannot read directory"}`, http.StatusInternalServerError)
return
}
// 4) Construire les slices JSON
var dirs []dirView
var medias []mediaItemView
thumbDir := filepath.Join("static", "thumbs")
os.MkdirAll(thumbDir, 0755)
for _, e := range entries {
name := e.Name()
full := filepath.Join(current, name)
if e.IsDir() {
dirs = append(dirs, dirView{
Name: name,
SubPath: filepath.ToSlash(filepath.Join(sub, name)),
})
continue
}
ext := strings.ToLower(filepath.Ext(name))
if !allowed[ext] {
continue
}
view := mediaItemView{
Title: name,
FilePath: full,
}
// Si c'est une vidéo, extraire métadonnées + screenshot
if ext == ".mkv" || ext == ".avi" || ext == ".mp4" || ext == ".mov" {
// Métadonnées via ffprobe
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
info, _ := probe(ctx, full)
cancel()
if info != nil {
// durée
if d, err := strconv.ParseFloat(info.Format.Duration, 64); err == nil {
secs := int64(d)
view.Duration = secs
view.DurationFmt = fmt.Sprintf("%d:%02d", secs/60, secs%60)
}
// résolution
for _, s := range info.Streams {
if s.CodecType == "video" {
view.Width = s.Width
view.Height = s.Height
break
}
}
}
// Génération du thumbnail
base := strings.TrimSuffix(name, ext)
thumbName := base + ".jpg"
thumbPath := filepath.Join(thumbDir, thumbName)
if _, err := os.Stat(thumbPath); os.IsNotExist(err) {
exec.Command("ffmpeg", "-ss", "5", "-i", full, "-frames:v", "1", thumbPath).Run()
}
view.ThumbURL = "/static/thumbs/" + thumbName
} else {
// Icônes génériques pour images/PDF/EPUB/CBZ
view.ThumbURL = "/static/icons/" + ext[1:] + ".svg"
}
medias = append(medias, view)
}
// 5) Réponse JSON
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"dirs": dirs,
"mediaItems": medias,
})
}
}