test
This commit is contained in:
parent
3fc81e5c80
commit
3409091557
@ -223,6 +223,10 @@ func RoutesProtected(r *mux.Router, bd *gorm.DB) {
|
||||
r.HandleFunc("/folders", renders.StreamHandler)
|
||||
r.HandleFunc("/folders/detail", renders.DetailHandler).Methods("GET")
|
||||
|
||||
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("/media/{partID:[0-9]+}", renders.MediaDetail(bd)).Methods("GET")
|
||||
|
||||
//API Scan folder
|
||||
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@ -839,7 +840,136 @@ func getAllPaths(db *gorm.DB) []*models.PathDownload {
|
||||
return paths
|
||||
}
|
||||
|
||||
type mediaItemView struct {
|
||||
models.MetadataItem
|
||||
MediaPartID int64
|
||||
}
|
||||
|
||||
// PathMedia renvoie la partial HTML de la grille de médias
|
||||
func PathMedia(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pid, _ := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
|
||||
|
||||
// 1) tentative depuis la BDD
|
||||
var items []mediaItemView
|
||||
db.Table("metadata_items").
|
||||
Select("metadata_items.*, media_parts.id AS media_part_id").
|
||||
Joins("JOIN media_items ON media_items.metadata_item_id = metadata_items.id").
|
||||
Joins("JOIN media_parts ON media_parts.media_item_id = media_items.id").
|
||||
Where("metadata_items.library_section_id = ?", pid).
|
||||
Scan(&items)
|
||||
|
||||
// 2) si rien en BDD, on scan physiquement le dossier et on crée un item minimal
|
||||
if len(items) == 0 {
|
||||
var pd models.PathDownload
|
||||
if err := db.First(&pd, pid).Error; err == nil {
|
||||
files, _ := filepath.Glob(filepath.Join(pd.Path, "*.*"))
|
||||
for _, f := range files {
|
||||
// on prend juste le nom de fichier comme titre
|
||||
items = append(items, mediaItemView{
|
||||
MetadataItem: models.MetadataItem{
|
||||
Title: filepath.Base(f),
|
||||
},
|
||||
MediaPartID: 0, // pas de part en BDD
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderPartial(w, "media_list", map[string]interface{}{
|
||||
"mediaItems": items,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MediaDetail affiche la page détail + player
|
||||
func MediaDetail(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
partID, _ := strconv.ParseInt(mux.Vars(r)["partID"], 10, 64)
|
||||
|
||||
// on récupère 1 metadata + file pour ce media_part
|
||||
var item struct {
|
||||
models.MetadataItem
|
||||
MediaPartID int64
|
||||
File string
|
||||
}
|
||||
db.Table("metadata_items").
|
||||
Select("metadata_items.*, media_parts.id AS media_part_id, media_parts.file").
|
||||
Joins("JOIN media_items ON media_items.metadata_item_id = metadata_items.id").
|
||||
Joins("JOIN media_parts ON media_parts.media_item_id = media_items.id").
|
||||
Where("media_parts.id = ?", partID).
|
||||
Scan(&item)
|
||||
|
||||
if item.MediaPartID == 0 {
|
||||
http.Error(w, "Média introuvable", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
renderTemplate(w, "media_detail", map[string]interface{}{
|
||||
"item": item,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Stream : transcode à la volée en MP4 progressif et pipe directement dans la réponse
|
||||
func Stream(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
partID, _ := strconv.ParseInt(mux.Vars(r)["partID"], 10, 64)
|
||||
var part models.MediaPart
|
||||
if err := db.First(&part, partID).Error; err != nil {
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "video/mp4")
|
||||
// ffmpeg en pipe
|
||||
cmd := exec.CommandContext(r.Context(),
|
||||
"ffmpeg",
|
||||
"-i", part.File,
|
||||
"-c:v", "libx264",
|
||||
"-c:a", "aac",
|
||||
"-movflags", "frag_keyframe+empty_moov+faststart",
|
||||
"-f", "mp4",
|
||||
"pipe:1",
|
||||
)
|
||||
cmd.Stdout = w
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Println("ffmpeg:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// FfmpegProbeOutput reflète la partie “format” et “streams” de ffprobe JSON
|
||||
type ffprobeOut struct {
|
||||
Format struct {
|
||||
Duration string `json:"duration"`
|
||||
} `json:"format"`
|
||||
Streams []struct {
|
||||
CodecType string `json:"codec_type"`
|
||||
Width int `json:"width,omitempty"`
|
||||
Height int `json:"height,omitempty"`
|
||||
} `json:"streams"`
|
||||
}
|
||||
|
||||
// appelle ffprobe et parse le JSON
|
||||
func probe(ctx context.Context, file string) (*ffprobeOut, error) {
|
||||
cmd := exec.CommandContext(ctx,
|
||||
"ffprobe", "-v", "error",
|
||||
"-print_format", "json",
|
||||
"-show_format", "-show_streams",
|
||||
file,
|
||||
)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var info ffprobeOut
|
||||
if err := json.Unmarshal(out, &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -85,4 +85,12 @@ function hide(target){
|
||||
document.addEventListener("htmx:afterOnLoad", function (event) {
|
||||
// console.log("Réponse du serveur :", event.detail.xhr.responseText);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.path-link').forEach(el=>{
|
||||
el.addEventListener('click', e=>{
|
||||
e.preventDefault();
|
||||
let id = el.dataset.id;
|
||||
fetch('/api/paths/'+id+'/media')
|
||||
.then(r=>r.text())
|
||||
.then(html=> document.getElementById('content').innerHTML = html);
|
||||
});
|
||||
});
|
||||
|
||||
@ -29,16 +29,15 @@
|
||||
<ul class="menu-list" id="libraryMenu" hidden>
|
||||
<li>
|
||||
<a class="is-active">Choise Library</a>
|
||||
<ul>
|
||||
<ul id="paths-list">
|
||||
{{range .paths}}
|
||||
<li>
|
||||
<span class="icon-text">
|
||||
<span><a>{{ .PathName }}</a></span>
|
||||
<span class="icon">
|
||||
<a style="padding-top: 0;height: 0;"><i class="fas fa-ellipsis-v"></i></a>
|
||||
</span>
|
||||
</span>
|
||||
{{end}}
|
||||
<a href="#" class="path-link" data-id="{{.ID}}">
|
||||
{{.PathName}}
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
11
templates/media_detail.pages.tmpl
Normal file
11
templates/media_detail.pages.tmpl
Normal file
@ -0,0 +1,11 @@
|
||||
{{define "media_detail"}}
|
||||
<div class="detail">
|
||||
<img src="{{.item.UserThumbURL}}" alt="{{.item.Title}}" class="cover">
|
||||
<h1>{{.item.Title}}</h1>
|
||||
{{with .item.Summary}}<p>{{.}}</p>{{end}}
|
||||
<video controls autoplay width="100%" poster="{{.item.UserThumbURL}}">
|
||||
<source src="/stream/{{.item.MediaPartID}}" type="video/mp4">
|
||||
Votre navigateur ne supporte pas la vidéo.
|
||||
</video>
|
||||
</div>
|
||||
{{end}}
|
||||
19
templates/media_list.pages.tmpl
Normal file
19
templates/media_list.pages.tmpl
Normal file
@ -0,0 +1,19 @@
|
||||
{{define "media_list"}}
|
||||
<div class="grid">
|
||||
{{if .mediaItems}}
|
||||
{{range .mediaItems}}
|
||||
<div class="card">
|
||||
<a href="/media/{{.MediaPartID}}">
|
||||
<img src="{{.UserThumbURL}}" alt="{{.Title}}" class="thumb" />
|
||||
<div class="infos">
|
||||
<h3>{{.Title}}</h3>
|
||||
{{with .Summary}}<p>{{.}}</p>{{end}}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p>Aucun média trouvé dans ce dossier.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
Loading…
Reference in New Issue
Block a user