This commit is contained in:
cangui 2025-06-21 19:55:53 +02:00
parent 12e9c11536
commit 6aebad2db7
2 changed files with 41 additions and 35 deletions

View File

@ -229,7 +229,7 @@ r.HandleFunc("/api/paths/{id:[0-9]+}/media", renders.PathMedia(bd)).Methods("GET
r.HandleFunc("/stream/{partID:[0-9]+}", renders.Stream(bd)).Methods("GET") r.HandleFunc("/stream/{partID:[0-9]+}", renders.Stream(bd)).Methods("GET")
r.HandleFunc("/media/{partID:[0-9]+}", renders.MediaDetail(bd)).Methods("GET") r.HandleFunc("/media/{partID:[0-9]+}", renders.MediaDetail(bd)).Methods("GET")
r.HandleFunc("/hls/{partID:[0-9]+}/{file}", renders.HLSStream(bd)).Methods("GET") r.HandleFunc("/hls/{partID:[0-9]+}/{file}", renders.HLSStream(bd)).Methods("GET")
r.HandleFunc("/hls/{partID:[0-9]+}/", renders.HLSStream(bd)).Methods("GET")
//API Scan folder //API Scan folder
} }

View File

@ -988,10 +988,11 @@ type mediaDetailView struct {
HLSURL string // ajouté HLSURL string // ajouté
} }
// MediaDetail affiche la page détail + player
// MediaDetail renvoie la partial HTML du détail dun média
func MediaDetail(db *gorm.DB) http.HandlerFunc { func MediaDetail(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// 1) Extraire le partID
partID, _ := strconv.ParseInt(mux.Vars(r)["partID"], 10, 64) partID, _ := strconv.ParseInt(mux.Vars(r)["partID"], 10, 64)
var view mediaDetailView var view mediaDetailView
@ -1000,8 +1001,8 @@ func MediaDetail(db *gorm.DB) http.HandlerFunc {
// --- CAS BDD --- // --- CAS BDD ---
var item struct { var item struct {
models.MetadataItem models.MetadataItem
MediaPartID int64 MediaPartID int64
File string File string
UserThumbURL string UserThumbURL string
} }
db.Table("metadata_items"). db.Table("metadata_items").
@ -1016,15 +1017,16 @@ func MediaDetail(db *gorm.DB) http.HandlerFunc {
return return
} }
// formater la durée // formatage durée
m := item.Duration / 60 m := item.Duration / 60
s := item.Duration % 60 s := item.Duration % 60
view = mediaDetailView{ view = mediaDetailView{
Title: item.Title, Title: item.Title,
Summary: item.Summary, Summary: item.Summary,
DurationFmt: strconv.FormatInt(m, 10) + ":" + fmt.Sprintf("%02d", s), DurationFmt: fmt.Sprintf("%d:%02d", m, s),
ThumbURL: item.UserThumbURL, ThumbURL: item.UserThumbURL,
HLSURL: fmt.Sprintf("/hls/%d/index.m3u8", partID), HLSURL: fmt.Sprintf("/hls/%d/index.m3u8", item.MediaPartID),
} }
} else { } else {
@ -1035,39 +1037,37 @@ func MediaDetail(db *gorm.DB) http.HandlerFunc {
return return
} }
// titre // base name et thumbnail
title := filepath.Base(path) title := filepath.Base(path)
// génère un thumbnail si besoin
ext := filepath.Ext(path) ext := filepath.Ext(path)
base := strings.TrimSuffix(filepath.Base(path), ext) base := strings.TrimSuffix(title, ext)
thumbDir := filepath.Join("static", "thumbs") thumbDir := filepath.Join("static", "thumbs")
os.MkdirAll(thumbDir, 0755) os.MkdirAll(thumbDir, 0755)
thumbPath := filepath.Join(thumbDir, base+".jpg") thumbPath := filepath.Join(thumbDir, base+".jpg")
if _, err := os.Stat(thumbPath); os.IsNotExist(err) { if _, err := os.Stat(thumbPath); os.IsNotExist(err) {
// screenshot au 5s // capture au 5s
exec.CommandContext(r.Context(), exec.CommandContext(context.Background(),
"ffmpeg", "-ss", "5", "-i", path, "-frames:v", "1", thumbPath, "ffmpeg", "-ss", "5", "-i", path, "-frames:v", "1", thumbPath,
).Run() ).Run()
} }
view = mediaDetailView{ view = mediaDetailView{
Title: title, Title: title,
Summary: "", // pas de résumé en FS-only Summary: "",
DurationFmt: "", // on ne probe pas ici DurationFmt: "",
ThumbURL: "/static/thumbs/" + base + ".jpg", ThumbURL: "/static/thumbs/" + base + ".jpg",
// on passe le path en query pour le streaming // **ici** on passe le path en query pour que HLSStream sache où chercher
HLSURL: "/hls/0/index.m3u8?path=" + url.QueryEscape(path), HLSURL: fmt.Sprintf("/hls/0/index.m3u8?path=%s", url.QueryEscape(path)),
} }
} }
// 3) Render partial dans #content
renderPartial(w, "media_detail", map[string]interface{}{ renderPartial(w, "media_detail", map[string]interface{}{
"item": view, "item": view,
}) })
} }
} }
// Stream : transcode à la volée en MP4 progressif et pipe directement dans la réponse // Stream : transcode à la volée en MP4 progressif et pipe directement dans la réponse
func Stream(db *gorm.DB) http.HandlerFunc { func Stream(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@ -1099,13 +1099,18 @@ func Stream(db *gorm.DB) http.HandlerFunc {
} }
// renders/media.go (ajoutez cette fonction) // renders/media.go (ajoutez cette fonction)
// rend le HLS pour BDD (partID>0) et FS-only (partID==0)
func HLSStream(db *gorm.DB) http.HandlerFunc { func HLSStream(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) // 1) identifier le partID et le dossier temporaire
partID, _ := strconv.ParseInt(vars["partID"], 10, 64) partID, _ := strconv.ParseInt(mux.Vars(r)["partID"], 10, 64)
tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("hls_%d", partID))
playlist := filepath.Join(tmpDir, "index.m3u8")
// Déterminer le fichier source // 2) déterminer s'il faut (re)générer
needGen := false
var filePath string var filePath string
if partID > 0 { if partID > 0 {
// cas BDD // cas BDD
var part models.MediaPart var part models.MediaPart
@ -1114,22 +1119,22 @@ func HLSStream(db *gorm.DB) http.HandlerFunc {
return return
} }
filePath = part.File filePath = part.File
needGen = true
} else { } else {
// cas FS-only // cas FS-only : on génère seulement si playlist manquante
filePath = r.URL.Query().Get("path") if _, err := os.Stat(playlist); os.IsNotExist(err) {
if filePath == "" { filePath = r.URL.Query().Get("path")
http.Error(w, "Média introuvable", http.StatusNotFound) if filePath == "" {
return http.Error(w, "Média introuvable", http.StatusNotFound)
return
}
needGen = true
} }
} }
// Préparer le dossier de cache HLS // 3) (Re)générer le HLS si besoin
tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("hls_%d", partID)) if needGen {
os.MkdirAll(tmpDir, 0755) os.MkdirAll(tmpDir, 0755)
playlist := filepath.Join(tmpDir, "index.m3u8")
// (Re)générer si nécessaire
if _, err := os.Stat(playlist); os.IsNotExist(err) {
cmd := exec.CommandContext(r.Context(), cmd := exec.CommandContext(r.Context(),
"ffmpeg", "ffmpeg",
"-i", filePath, "-i", filePath,
@ -1147,7 +1152,7 @@ func HLSStream(db *gorm.DB) http.HandlerFunc {
} }
} }
// Servir le dossier HLS // 4) servir **tout** tmpDir sous /hls/{partID}/…
prefix := fmt.Sprintf("/hls/%d/", partID) prefix := fmt.Sprintf("/hls/%d/", partID)
http.StripPrefix(prefix, http.StripPrefix(prefix,
http.FileServer(http.Dir(tmpDir)), http.FileServer(http.Dir(tmpDir)),
@ -1159,6 +1164,7 @@ func HLSStream(db *gorm.DB) http.HandlerFunc {
func renderPartial(w http.ResponseWriter, templ string, data map[string]interface{}) { func renderPartial(w http.ResponseWriter, templ string, data map[string]interface{}) {
// Exécute directement le define `<templ>.pages.tmpl` // Exécute directement le define `<templ>.pages.tmpl`
if err := templates.ExecuteTemplate(w, templ+".pages.tmpl", data); err != nil { if err := templates.ExecuteTemplate(w, templ+".pages.tmpl", data); err != nil {