Hotfix issues with 1.0.3
This commit is contained in:
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -72,5 +72,5 @@ body:
|
|||||||
label: Trace Logs have been provided as applicable
|
label: Trace Logs have been provided as applicable
|
||||||
description: Trace logs are **generally required** and are not optional for all bug reports and contain `trace`. Info logs are invalid for bug reports and do not contain `debug` nor `trace`
|
description: Trace logs are **generally required** and are not optional for all bug reports and contain `trace`. Info logs are invalid for bug reports and do not contain `debug` nor `trace`
|
||||||
options:
|
options:
|
||||||
- label: I have read and followed the steps in the wiki link above and provided the required trace logs - the logs contain `trace` - that are relevant and show this issue.
|
- label: I have read and followed the steps in the documentation link and provided the required trace logs - the logs contain `trace` - that are relevant and show this issue.
|
||||||
required: true
|
required: true
|
||||||
@@ -621,7 +621,7 @@ func (r *RealDebrid) CheckLink(link string) error {
|
|||||||
func (r *RealDebrid) _getDownloadLink(file *types.File) (*types.DownloadLink, error) {
|
func (r *RealDebrid) _getDownloadLink(file *types.File) (*types.DownloadLink, error) {
|
||||||
url := fmt.Sprintf("%s/unrestrict/link/", r.Host)
|
url := fmt.Sprintf("%s/unrestrict/link/", r.Host)
|
||||||
_link := file.Link
|
_link := file.Link
|
||||||
if strings.HasPrefix(_link, "https://real-debrid.com/d/") {
|
if strings.HasPrefix(file.Link, "https://real-debrid.com/d/") && len(file.Link) > 39 {
|
||||||
_link = file.Link[0:39]
|
_link = file.Link[0:39]
|
||||||
}
|
}
|
||||||
payload := gourl.Values{
|
payload := gourl.Values{
|
||||||
|
|||||||
@@ -137,10 +137,10 @@ func (c *Cache) refreshRclone() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 60 * time.Second,
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
MaxIdleConns: 10,
|
MaxIdleConns: 10,
|
||||||
IdleConnTimeout: 30 * time.Second,
|
IdleConnTimeout: 60 * time.Second,
|
||||||
DisableCompression: false,
|
DisableCompression: false,
|
||||||
MaxIdleConnsPerHost: 5,
|
MaxIdleConnsPerHost: 5,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func NewAccounts(debridConf config.Debrid) *Accounts {
|
|||||||
if token == "" {
|
if token == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
account := newAccount(token, idx)
|
account := newAccount(debridConf.Name, token, idx)
|
||||||
accounts = append(accounts, account)
|
accounts = append(accounts, account)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ func NewAccounts(debridConf config.Debrid) *Accounts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
|
Debrid string // e.g., "realdebrid", "torbox", etc.
|
||||||
Order int
|
Order int
|
||||||
Disabled bool
|
Disabled bool
|
||||||
Token string
|
Token string
|
||||||
@@ -176,30 +177,31 @@ func (a *Accounts) SetDownloadLinks(links map[string]*DownloadLink) {
|
|||||||
a.Current().setLinks(links)
|
a.Current().setLinks(links)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAccount(token string, index int) *Account {
|
func newAccount(debridName, token string, index int) *Account {
|
||||||
return &Account{
|
return &Account{
|
||||||
Token: token,
|
Debrid: debridName,
|
||||||
Order: index,
|
Token: token,
|
||||||
links: make(map[string]*DownloadLink),
|
Order: index,
|
||||||
|
links: make(map[string]*DownloadLink),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Account) getLink(fileLink string) (*DownloadLink, bool) {
|
func (a *Account) getLink(fileLink string) (*DownloadLink, bool) {
|
||||||
a.mu.RLock()
|
a.mu.RLock()
|
||||||
defer a.mu.RUnlock()
|
defer a.mu.RUnlock()
|
||||||
dl, ok := a.links[fileLink[0:39]]
|
dl, ok := a.links[a.sliceFileLink(fileLink)]
|
||||||
return dl, ok
|
return dl, ok
|
||||||
}
|
}
|
||||||
func (a *Account) setLink(fileLink string, dl *DownloadLink) {
|
func (a *Account) setLink(fileLink string, dl *DownloadLink) {
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer a.mu.Unlock()
|
||||||
a.links[fileLink[0:39]] = dl
|
a.links[a.sliceFileLink(fileLink)] = dl
|
||||||
}
|
}
|
||||||
func (a *Account) deleteLink(fileLink string) {
|
func (a *Account) deleteLink(fileLink string) {
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer a.mu.Unlock()
|
||||||
|
|
||||||
delete(a.links, fileLink[0:39])
|
delete(a.links, a.sliceFileLink(fileLink))
|
||||||
}
|
}
|
||||||
func (a *Account) resetDownloadLinks() {
|
func (a *Account) resetDownloadLinks() {
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
@@ -225,6 +227,17 @@ func (a *Account) setLinks(links map[string]*DownloadLink) {
|
|||||||
// Expired, continue
|
// Expired, continue
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
a.links[dl.Link[0:39]] = dl
|
a.links[a.sliceFileLink(dl.Link)] = dl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// slice download link
|
||||||
|
func (a *Account) sliceFileLink(fileLink string) string {
|
||||||
|
if a.Debrid != "realdebrid" {
|
||||||
|
return fileLink
|
||||||
|
}
|
||||||
|
if len(fileLink) < 39 {
|
||||||
|
return fileLink
|
||||||
|
}
|
||||||
|
return fileLink[0:39]
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,26 +75,6 @@ type Job struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *Job) getUnprocessedBrokenItems() map[string][]arr.ContentFile {
|
|
||||||
items := make(map[string][]arr.ContentFile)
|
|
||||||
|
|
||||||
for arrName, files := range j.BrokenItems {
|
|
||||||
if len(files) == 0 {
|
|
||||||
continue // Skip empty file lists
|
|
||||||
}
|
|
||||||
items[arrName] = make([]arr.ContentFile, 0, len(files))
|
|
||||||
for _, file := range files {
|
|
||||||
if file.Path != "" && file.TargetPath != "" && !file.Processed {
|
|
||||||
items[arrName] = append(items[arrName], file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(items) == 0 {
|
|
||||||
return nil // Return nil if no unprocessed items found
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(arrs *arr.Storage, engine *debrid.Storage) *Repair {
|
func New(arrs *arr.Storage, engine *debrid.Storage) *Repair {
|
||||||
cfg := config.Get()
|
cfg := config.Get()
|
||||||
workers := runtime.NumCPU() * 20
|
workers := runtime.NumCPU() * 20
|
||||||
@@ -765,7 +745,7 @@ func (r *Repair) ProcessJob(id string) error {
|
|||||||
return fmt.Errorf("job %s already failed", id)
|
return fmt.Errorf("job %s already failed", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
brokenItems := job.getUnprocessedBrokenItems()
|
brokenItems := job.BrokenItems
|
||||||
if len(brokenItems) == 0 {
|
if len(brokenItems) == 0 {
|
||||||
r.logger.Info().Msgf("No broken items found for job %s", id)
|
r.logger.Info().Msgf("No broken items found for job %s", id)
|
||||||
job.CompletedAt = time.Now()
|
job.CompletedAt = time.Now()
|
||||||
@@ -773,144 +753,63 @@ func (r *Repair) ProcessJob(id string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
r.logger.Info().Msgf("Processing job %s with %d broken items", id, len(brokenItems))
|
|
||||||
go r.processJob(job, brokenItems)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repair) processJob(job *Job, brokenItems map[string][]arr.ContentFile) {
|
|
||||||
if job.ctx == nil || job.ctx.Err() != nil {
|
if job.ctx == nil || job.ctx.Err() != nil {
|
||||||
job.ctx, job.cancelFunc = context.WithCancel(r.ctx)
|
job.ctx, job.cancelFunc = context.WithCancel(r.ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := make([]error, 0)
|
g, ctx := errgroup.WithContext(job.ctx)
|
||||||
processedCount := 0
|
g.SetLimit(r.workers)
|
||||||
|
|
||||||
for arrName, items := range brokenItems {
|
for arrName, items := range brokenItems {
|
||||||
select {
|
items := items
|
||||||
case <-job.ctx.Done():
|
arrName := arrName
|
||||||
r.logger.Info().Msgf("Job %s cancelled", job.ID)
|
g.Go(func() error {
|
||||||
job.Status = JobCancelled
|
|
||||||
job.CompletedAt = time.Now()
|
|
||||||
job.Error = "Job was cancelled"
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
// Continue processing
|
|
||||||
}
|
|
||||||
|
|
||||||
a := r.arrs.Get(arrName)
|
select {
|
||||||
if a == nil {
|
case <-ctx.Done():
|
||||||
errs = append(errs, fmt.Errorf("arr %s not found", arrName))
|
return ctx.Err()
|
||||||
continue
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.DeleteFiles(items); err != nil {
|
a := r.arrs.Get(arrName)
|
||||||
errs = append(errs, fmt.Errorf("failed to delete broken items for %s: %w", arrName, err))
|
if a == nil {
|
||||||
continue
|
r.logger.Error().Msgf("Arr %s not found", arrName)
|
||||||
}
|
return nil
|
||||||
// Search for missing items
|
}
|
||||||
if err := a.SearchMissing(items); err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("failed to search missing items for %s: %w", arrName, err))
|
if err := a.DeleteFiles(items); err != nil {
|
||||||
continue
|
r.logger.Error().Err(err).Msgf("Failed to delete broken items for %s", arrName)
|
||||||
}
|
return nil
|
||||||
processedCount += len(items)
|
}
|
||||||
// Mark this item as processed
|
// Search for missing items
|
||||||
for i := range items {
|
if err := a.SearchMissing(items); err != nil {
|
||||||
items[i].Processed = true
|
r.logger.Error().Err(err).Msgf("Failed to search missing items for %s", arrName)
|
||||||
}
|
return nil
|
||||||
job.BrokenItems[arrName] = items
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update job status to in-progress
|
// Update job status to in-progress
|
||||||
job.Status = JobProcessing
|
job.Status = JobProcessing
|
||||||
r.saveToFile()
|
r.saveToFile()
|
||||||
|
|
||||||
if len(errs) > 0 {
|
// Launch a goroutine to wait for completion and update the job
|
||||||
errMsg := fmt.Sprintf("Job %s encountered errors: %v", job.ID, errs)
|
go func() {
|
||||||
job.Error = errMsg
|
if err := g.Wait(); err != nil {
|
||||||
job.FailedAt = time.Now()
|
job.FailedAt = time.Now()
|
||||||
job.Status = JobFailed
|
job.Error = err.Error()
|
||||||
r.logger.Error().Msg(errMsg)
|
job.CompletedAt = time.Now()
|
||||||
go func() {
|
job.Status = JobFailed
|
||||||
if err := request.SendDiscordMessage("repair_failed", "error", job.discordContext()); err != nil {
|
r.logger.Error().Err(err).Msgf("Job %s failed", id)
|
||||||
r.logger.Error().Msgf("Error sending discord message: %v", err)
|
} else {
|
||||||
}
|
job.CompletedAt = time.Now()
|
||||||
}()
|
job.Status = JobCompleted
|
||||||
return
|
r.logger.Info().Msgf("Job %s completed successfully", id)
|
||||||
}
|
|
||||||
remainingItems := job.getUnprocessedBrokenItems()
|
|
||||||
if len(remainingItems) == 0 {
|
|
||||||
// All items processed, mark job as completed
|
|
||||||
job.CompletedAt = time.Now()
|
|
||||||
job.Status = JobCompleted
|
|
||||||
r.logger.Info().Msgf("Job %s completed successfully (all items processed)", job.ID)
|
|
||||||
go func() {
|
|
||||||
if err := request.SendDiscordMessage("repair_complete", "success", job.discordContext()); err != nil {
|
|
||||||
r.logger.Error().Msgf("Error sending discord message: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
} else {
|
|
||||||
// Some items still remain, keep job as pending
|
|
||||||
job.Status = JobPending
|
|
||||||
r.logger.Info().Msgf("Job %s: processed %d selected items successfully, %d items remaining", job.ID, processedCount, len(remainingItems))
|
|
||||||
go func() {
|
|
||||||
if err := request.SendDiscordMessage("repair_partial_complete", "info", job.discordContext()); err != nil {
|
|
||||||
r.logger.Error().Msgf("Error sending discord message: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
r.saveToFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessJobItems processes the selected items for a job
|
|
||||||
// selectedItems is the map of arr names to the list of file IDs to process
|
|
||||||
func (r *Repair) ProcessJobItems(id string, selectedItems map[string][]int) error {
|
|
||||||
job := r.GetJob(id)
|
|
||||||
if job == nil {
|
|
||||||
return fmt.Errorf("job %s not found", id)
|
|
||||||
}
|
|
||||||
if job.Status != JobPending {
|
|
||||||
return fmt.Errorf("job %s not pending", id)
|
|
||||||
}
|
|
||||||
if job.StartedAt.IsZero() {
|
|
||||||
return fmt.Errorf("job %s not started", id)
|
|
||||||
}
|
|
||||||
if !job.CompletedAt.IsZero() {
|
|
||||||
return fmt.Errorf("job %s already completed", id)
|
|
||||||
}
|
|
||||||
if !job.FailedAt.IsZero() {
|
|
||||||
return fmt.Errorf("job %s already failed", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
brokenItems := job.getUnprocessedBrokenItems()
|
|
||||||
validatedItems := make(map[string][]arr.ContentFile)
|
|
||||||
|
|
||||||
for arrName, selectedItemsList := range selectedItems {
|
|
||||||
if jobItems, exists := brokenItems[arrName]; exists {
|
|
||||||
validItems := make([]arr.ContentFile, 0, len(selectedItemsList))
|
|
||||||
for _, item := range selectedItemsList {
|
|
||||||
// Find the item in the job items
|
|
||||||
for _, jobItem := range jobItems {
|
|
||||||
if jobItem.FileId == item {
|
|
||||||
validItems = append(validItems, jobItem)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(validItems) > 0 {
|
|
||||||
validatedItems[arrName] = validItems
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if len(validatedItems) == 0 {
|
|
||||||
return fmt.Errorf("no valid items found for job %s", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
job.Status = JobProcessing
|
r.saveToFile()
|
||||||
r.saveToFile()
|
}()
|
||||||
|
|
||||||
go r.processJob(job, validatedItems)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -326,28 +326,6 @@ func (wb *Web) handleProcessRepairJob(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wb *Web) handleProcessRepairJobItems(w http.ResponseWriter, r *http.Request) {
|
|
||||||
id := chi.URLParam(r, "id")
|
|
||||||
if id == "" {
|
|
||||||
http.Error(w, "No job ID provided", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req struct {
|
|
||||||
Items map[string][]int `json:"items"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_store := store.Get()
|
|
||||||
if err := _store.Repair().ProcessJobItems(id, req.Items); err != nil {
|
|
||||||
wb.logger.Error().Err(err).Msg("Failed to process repair job items")
|
|
||||||
http.Error(w, "Failed to process job items: "+err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wb *Web) handleDeleteRepairJob(w http.ResponseWriter, r *http.Request) {
|
func (wb *Web) handleDeleteRepairJob(w http.ResponseWriter, r *http.Request) {
|
||||||
// Read ids from body
|
// Read ids from body
|
||||||
var req struct {
|
var req struct {
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ func (wb *Web) Routes() http.Handler {
|
|||||||
r.Post("/repair", wb.handleRepairMedia)
|
r.Post("/repair", wb.handleRepairMedia)
|
||||||
r.Get("/repair/jobs", wb.handleGetRepairJobs)
|
r.Get("/repair/jobs", wb.handleGetRepairJobs)
|
||||||
r.Post("/repair/jobs/{id}/process", wb.handleProcessRepairJob)
|
r.Post("/repair/jobs/{id}/process", wb.handleProcessRepairJob)
|
||||||
r.Post("/repair/jobs/{id}/process-items", wb.handleProcessRepairJobItems)
|
|
||||||
r.Post("/repair/jobs/{id}/stop", wb.handleStopRepairJob)
|
r.Post("/repair/jobs/{id}/stop", wb.handleStopRepairJob)
|
||||||
r.Delete("/repair/jobs", wb.handleDeleteRepairJob)
|
r.Delete("/repair/jobs", wb.handleDeleteRepairJob)
|
||||||
r.Get("/torrents", wb.handleGetTorrents)
|
r.Get("/torrents", wb.handleGetTorrents)
|
||||||
|
|||||||
@@ -1005,6 +1005,9 @@
|
|||||||
if (config.max_file_size) {
|
if (config.max_file_size) {
|
||||||
document.querySelector('[name="max_file_size"]').value = config.max_file_size;
|
document.querySelector('[name="max_file_size"]').value = config.max_file_size;
|
||||||
}
|
}
|
||||||
|
if (config.remove_stalled_after) {
|
||||||
|
document.querySelector('[name="remove_stalled_after"]').value = config.remove_stalled_after;
|
||||||
|
}
|
||||||
if (config.discord_webhook_url) {
|
if (config.discord_webhook_url) {
|
||||||
document.querySelector('[name="discord_webhook_url"]').value = config.discord_webhook_url;
|
document.querySelector('[name="discord_webhook_url"]').value = config.discord_webhook_url;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,13 +130,7 @@
|
|||||||
<h6 class="mb-0">
|
<h6 class="mb-0">
|
||||||
Broken Items
|
Broken Items
|
||||||
<span class="badge bg-secondary" id="totalItemsCount">0</span>
|
<span class="badge bg-secondary" id="totalItemsCount">0</span>
|
||||||
<span class="badge bg-primary" id="selectedItemsCount">0 selected</span>
|
|
||||||
</h6>
|
</h6>
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button class="btn btn-sm btn-primary" id="processSelectedItemsBtn" disabled>
|
|
||||||
<i class="bi bi-play-fill me-1"></i>Process Selected
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters and Search -->
|
<!-- Filters and Search -->
|
||||||
@@ -171,11 +165,6 @@
|
|||||||
<table class="table table-sm table-striped table-hover">
|
<table class="table table-sm table-striped table-hover">
|
||||||
<thead class="sticky-top">
|
<thead class="sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 40px;">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="selectAllItemsTable">
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th>Arr</th>
|
<th>Arr</th>
|
||||||
<th>Path</th>
|
<th>Path</th>
|
||||||
<th style="width: 100px;">Type</th>
|
<th style="width: 100px;">Type</th>
|
||||||
@@ -294,7 +283,7 @@
|
|||||||
|
|
||||||
if (!response.ok) throw new Error(await response.text());
|
if (!response.ok) throw new Error(await response.text());
|
||||||
createToast('Repair process initiated successfully!');
|
createToast('Repair process initiated successfully!');
|
||||||
loadJobs(1); // Refresh jobs after submission
|
await loadJobs(1); // Refresh jobs after submission
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
createToast(`Error starting repair: ${error.message}`, 'error');
|
createToast(`Error starting repair: ${error.message}`, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -391,12 +380,6 @@
|
|||||||
let totalItems = job.broken_items ? Object.values(job.broken_items).reduce((sum, arr) => sum + arr.length, 0) : 0;
|
let totalItems = job.broken_items ? Object.values(job.broken_items).reduce((sum, arr) => sum + arr.length, 0) : 0;
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input job-checkbox" type="checkbox" value="${job.id}"
|
|
||||||
${canDelete ? '' : 'disabled'} data-can-delete="${canDelete}">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td><a href="#" class="text-link view-job" data-id="${job.id}"><small>${job.id.substring(0, 8)}</small></a></td>
|
<td><a href="#" class="text-link view-job" data-id="${job.id}"><small>${job.id.substring(0, 8)}</small></a></td>
|
||||||
<td>${job.arrs.join(', ')}</td>
|
<td>${job.arrs.join(', ')}</td>
|
||||||
<td><small>${formattedDate}</small></td>
|
<td><small>${formattedDate}</small></td>
|
||||||
@@ -459,9 +442,8 @@
|
|||||||
document.querySelectorAll('#jobsPagination a[data-page]').forEach(link => {
|
document.querySelectorAll('#jobsPagination a[data-page]').forEach(link => {
|
||||||
link.addEventListener('click', (e) => {
|
link.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const newPage = parseInt(e.currentTarget.dataset.page);
|
currentPage = parseInt(e.currentTarget.dataset.page);
|
||||||
currentPage = newPage;
|
renderJobsTable(currentPage);
|
||||||
renderJobsTable(newPage);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -526,7 +508,6 @@
|
|||||||
// modal functions
|
// modal functions
|
||||||
function processItemsData(brokenItems) {
|
function processItemsData(brokenItems) {
|
||||||
const items = [];
|
const items = [];
|
||||||
let itemId = 0;
|
|
||||||
|
|
||||||
for (const [arrName, itemsArray] of Object.entries(brokenItems)) {
|
for (const [arrName, itemsArray] of Object.entries(brokenItems)) {
|
||||||
if (itemsArray && itemsArray.length > 0) {
|
if (itemsArray && itemsArray.length > 0) {
|
||||||
@@ -601,51 +582,15 @@
|
|||||||
row.dataset.itemId = item.id;
|
row.dataset.itemId = item.id;
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input item-checkbox"
|
|
||||||
type="checkbox"
|
|
||||||
value="${item.id}"
|
|
||||||
${selectedItems.has(item.id) ? 'checked' : ''}>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td><span class="badge bg-info">${item.arr}</span></td>
|
<td><span class="badge bg-info">${item.arr}</span></td>
|
||||||
<td><small class="text-muted" title="${item.path}">${item.path}</small></td>
|
<td><small class="text-muted" title="${item.path}">${item.path}</small></td>
|
||||||
<td><span class="badge ${item.type === 'movie' ? 'bg-primary' : item.type === 'tv' ? 'bg-success' : 'bg-secondary'}">${item.type}</span></td>
|
<td><span class="badge ${item.type === 'movie' ? 'bg-primary' : item.type === 'tv' ? 'bg-success' : 'bg-secondary'}">${item.type}</span></td>
|
||||||
<td><small>${formatFileSize(item.size)}</small></td>
|
<td><small>${formatFileSize(item.size)}</small></td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Make row clickable to toggle selection
|
|
||||||
row.addEventListener('click', (e) => {
|
|
||||||
if (e.target.type !== 'checkbox') {
|
|
||||||
const checkbox = row.querySelector('.item-checkbox');
|
|
||||||
checkbox.checked = !checkbox.checked;
|
|
||||||
checkbox.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tableBody.appendChild(row);
|
tableBody.appendChild(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listeners to checkboxes
|
|
||||||
document.querySelectorAll('.item-checkbox').forEach(checkbox => {
|
|
||||||
checkbox.addEventListener('change', (e) => {
|
|
||||||
const itemId = parseInt(e.target.value);
|
|
||||||
const row = e.target.closest('tr');
|
|
||||||
|
|
||||||
if (e.target.checked) {
|
|
||||||
selectedItems.add(itemId);
|
|
||||||
row.classList.add('selected');
|
|
||||||
} else {
|
|
||||||
selectedItems.delete(itemId);
|
|
||||||
row.classList.remove('selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
updateItemsStats();
|
|
||||||
updateSelectAllStates();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create pagination
|
// Create pagination
|
||||||
if (totalPages > 1) {
|
if (totalPages > 1) {
|
||||||
const prevLi = document.createElement('li');
|
const prevLi = document.createElement('li');
|
||||||
@@ -674,45 +619,18 @@
|
|||||||
document.querySelectorAll('#itemsPagination a[data-items-page]').forEach(link => {
|
document.querySelectorAll('#itemsPagination a[data-items-page]').forEach(link => {
|
||||||
link.addEventListener('click', (e) => {
|
link.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const newPage = parseInt(e.currentTarget.dataset.itemsPage);
|
currentItemsPage = parseInt(e.currentTarget.dataset.itemsPage);;
|
||||||
currentItemsPage = newPage;
|
|
||||||
renderBrokenItemsTable();
|
renderBrokenItemsTable();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
updateSelectAllStates();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateItemsStats() {
|
function updateItemsStats() {
|
||||||
document.getElementById('totalItemsCount').textContent = allBrokenItems.length;
|
document.getElementById('totalItemsCount').textContent = allBrokenItems.length;
|
||||||
document.getElementById('selectedItemsCount').textContent = `${selectedItems.size} selected`;
|
|
||||||
|
|
||||||
const processSelectedBtn = document.getElementById('processSelectedItemsBtn');
|
|
||||||
processSelectedBtn.disabled = selectedItems.size === 0;
|
|
||||||
|
|
||||||
// Update footer stats
|
// Update footer stats
|
||||||
const footerStats = document.getElementById('modalFooterStats');
|
const footerStats = document.getElementById('modalFooterStats');
|
||||||
footerStats.textContent = `Total: ${allBrokenItems.length} | Filtered: ${filteredItems.length} | Selected: ${selectedItems.size}`;
|
footerStats.textContent = `Total: ${allBrokenItems.length} | Filtered: ${filteredItems.length}`;
|
||||||
}
|
|
||||||
|
|
||||||
function updateSelectAllStates() {
|
|
||||||
const selectAllTableCheckbox = document.getElementById('selectAllItemsTable');
|
|
||||||
const visibleCheckboxes = document.querySelectorAll('.item-checkbox');
|
|
||||||
const checkedVisible = document.querySelectorAll('.item-checkbox:checked');
|
|
||||||
|
|
||||||
if (visibleCheckboxes.length === 0) {
|
|
||||||
selectAllTableCheckbox.indeterminate = false;
|
|
||||||
selectAllTableCheckbox.checked = false;
|
|
||||||
} else if (checkedVisible.length === visibleCheckboxes.length) {
|
|
||||||
selectAllTableCheckbox.indeterminate = false;
|
|
||||||
selectAllTableCheckbox.checked = true;
|
|
||||||
} else if (checkedVisible.length > 0) {
|
|
||||||
selectAllTableCheckbox.indeterminate = true;
|
|
||||||
selectAllTableCheckbox.checked = false;
|
|
||||||
} else {
|
|
||||||
selectAllTableCheckbox.indeterminate = false;
|
|
||||||
selectAllTableCheckbox.checked = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateArrFilter() {
|
function populateArrFilter() {
|
||||||
@@ -728,62 +646,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('selectAllItemsTable').addEventListener('change', (e) => {
|
|
||||||
const visibleCheckboxes = document.querySelectorAll('.item-checkbox');
|
|
||||||
visibleCheckboxes.forEach(checkbox => {
|
|
||||||
const itemId = parseInt(checkbox.value);
|
|
||||||
const row = checkbox.closest('tr');
|
|
||||||
|
|
||||||
if (e.target.checked) {
|
|
||||||
selectedItems.add(itemId);
|
|
||||||
checkbox.checked = true;
|
|
||||||
row.classList.add('selected');
|
|
||||||
} else {
|
|
||||||
selectedItems.delete(itemId);
|
|
||||||
checkbox.checked = false;
|
|
||||||
row.classList.remove('selected');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
updateItemsStats();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('processSelectedItemsBtn').addEventListener('click', async () => {
|
|
||||||
if (selectedItems.size === 0) return;
|
|
||||||
|
|
||||||
const selectedItemsData = allBrokenItems.filter(item => selectedItems.has(item.id));
|
|
||||||
|
|
||||||
// Group by arr
|
|
||||||
const itemsByArr = {};
|
|
||||||
selectedItemsData.forEach(item => {
|
|
||||||
if (!itemsByArr[item.arr]) {
|
|
||||||
itemsByArr[item.arr] = [];
|
|
||||||
}
|
|
||||||
itemsByArr[item.arr].push(item.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(itemsByArr);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetcher(`/api/repair/jobs/${currentJob.id}/process-items`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ items: itemsByArr })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error(await response.text());
|
|
||||||
createToast(`Processing ${selectedItems.size} selected items`);
|
|
||||||
|
|
||||||
// Close modal and refresh jobs
|
|
||||||
const modal = bootstrap.Modal.getInstance(document.getElementById('jobDetailsModal'));
|
|
||||||
modal.hide();
|
|
||||||
loadJobs(currentPage);
|
|
||||||
} catch (error) {
|
|
||||||
createToast(`Error processing selected items: ${error.message}`, 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter event listeners
|
// Filter event listeners
|
||||||
document.getElementById('itemSearchInput').addEventListener('input', applyFilters);
|
document.getElementById('itemSearchInput').addEventListener('input', applyFilters);
|
||||||
document.getElementById('arrFilterSelect').addEventListener('change', applyFilters);
|
document.getElementById('arrFilterSelect').addEventListener('change', applyFilters);
|
||||||
|
|||||||
@@ -238,17 +238,5 @@ func setVideoResponseHeaders(w http.ResponseWriter, resp *http.Response, isRange
|
|||||||
w.Header().Set("Content-Range", contentRange)
|
w.Header().Set("Content-Range", contentRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video streaming optimizations
|
|
||||||
w.Header().Set("Accept-Ranges", "bytes") // Enable seeking
|
|
||||||
w.Header().Set("Connection", "keep-alive") // Keep connection open
|
|
||||||
|
|
||||||
// Prevent buffering in proxies/CDNs
|
|
||||||
w.Header().Set("X-Accel-Buffering", "no") // Nginx
|
|
||||||
w.Header().Set("Proxy-Buffering", "off") // General proxy
|
|
||||||
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Range")
|
|
||||||
w.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Range")
|
|
||||||
|
|
||||||
w.WriteHeader(resp.StatusCode)
|
w.WriteHeader(resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user