feat(lists): read Plaintext from file on disk (#2031)

* feat(lists): read Plaintext from file on disk

* feat(lists): add tooltip to plaintext url field
This commit is contained in:
ze0s 2025-04-13 17:40:38 +02:00 committed by GitHub
parent 40aa3b404b
commit a8c4114d6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 28 deletions

View file

@ -7,6 +7,9 @@ import (
"context" "context"
"io" "io"
"net/http" "net/http"
"net/url"
"os"
"runtime"
"strings" "strings"
"github.com/autobrr/autobrr/internal/domain" "github.com/autobrr/autobrr/internal/domain"
@ -23,33 +26,69 @@ func (s *service) plaintext(ctx context.Context, list *domain.List) error {
l.Debug().Msgf("fetching titles from %s", list.URL) l.Debug().Msgf("fetching titles from %s", list.URL)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, list.URL, nil) // Parse the URL to determine if it's a file or HTTP scheme
parsedURL, err := url.Parse(list.URL)
if err != nil { if err != nil {
return errors.Wrapf(err, "could not make new request for URL: %s", list.URL) return errors.Wrapf(err, "failed to parse URL: %s", list.URL)
} }
list.SetRequestHeaders(req) var body []byte
//setUserAgent(req) // Handle different URL schemes
switch parsedURL.Scheme {
case "file":
// Read from filesystem for file:// URLs
filePath := parsedURL.Path
resp, err := s.httpClient.Do(req) if runtime.GOOS == "windows" {
if err != nil { // On Windows, remove leading slash from path if needed
return errors.Wrapf(err, "failed to fetch titles from URL: %s", list.URL) if len(filePath) > 0 && filePath[0] == '/' && len(parsedURL.Host) > 0 {
} filePath = parsedURL.Host + filePath
defer resp.Body.Close() } else if len(filePath) > 0 && filePath[0] == '/' {
filePath = filePath[1:]
}
}
if resp.StatusCode != http.StatusOK { l.Debug().Msgf("reading from file: %s", filePath)
return errors.Wrapf(err, "failed to fetch titles from URL: %s", list.URL)
}
contentType := resp.Header.Get("Content-Type") body, err = os.ReadFile(filePath)
if !strings.HasPrefix(contentType, "text/plain") { if err != nil {
return errors.Wrapf(err, "unexpected content type for URL: %s expected text/plain got %s", list.URL, contentType) return errors.Wrapf(err, "failed to read file: %s", filePath)
} }
body, err := io.ReadAll(resp.Body) case "http", "https":
if err != nil { // Use HTTP client for http:// or https:// URLs
return errors.Wrapf(err, "failed to read response body from URL: %s", list.URL) req, err := http.NewRequestWithContext(ctx, http.MethodGet, list.URL, nil)
if err != nil {
return errors.Wrapf(err, "could not make new request for URL: %s", list.URL)
}
list.SetRequestHeaders(req)
//setUserAgent(req)
resp, err := s.httpClient.Do(req)
if err != nil {
return errors.Wrapf(err, "failed to fetch titles from URL: %s", list.URL)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.Wrapf(err, "failed to fetch titles from URL: %s with status code: %d", list.URL, resp.StatusCode)
}
contentType := resp.Header.Get("Content-Type")
if !strings.HasPrefix(contentType, "text/plain") {
return errors.Errorf("unexpected content type for URL: %s expected text/plain got %s", list.URL, contentType)
}
body, err = io.ReadAll(resp.Body)
if err != nil {
return errors.Wrapf(err, "failed to read response body from URL: %s", list.URL)
}
default:
return errors.Errorf("unsupported URL scheme: %s", parsedURL.Scheme)
} }
var titles []string var titles []string
@ -59,22 +98,17 @@ func (s *service) plaintext(ctx context.Context, list *domain.List) error {
if title == "" { if title == "" {
continue continue
} }
titles = append(titles, title) titles = append(titles, processTitle(title, list.MatchRelease)...)
} }
filterTitles := []string{} if len(titles) == 0 {
for _, title := range titles {
filterTitles = append(filterTitles, processTitle(title, list.MatchRelease)...)
}
if len(filterTitles) == 0 {
l.Debug().Msgf("no titles found to update for list: %v", list.Name) l.Debug().Msgf("no titles found to update for list: %v", list.Name)
return nil return nil
} }
joinedTitles := strings.Join(filterTitles, ",") joinedTitles := strings.Join(titles, ",")
l.Trace().Str("titles", joinedTitles).Msgf("found %d titles", len(joinedTitles)) l.Trace().Str("titles", joinedTitles).Msgf("found %d titles", len(titles))
filterUpdate := domain.FilterUpdate{Shows: &joinedTitles} filterUpdate := domain.FilterUpdate{Shows: &joinedTitles}

View file

@ -63,6 +63,7 @@ import { DocsTooltip } from "@components/tooltips/DocsTooltip";
import { MultiSelect as RMSC } from "react-multi-select-component"; import { MultiSelect as RMSC } from "react-multi-select-component";
import { useToggle } from "@hooks/hooks.ts"; import { useToggle } from "@hooks/hooks.ts";
import { DeleteModal } from "@components/modals"; import { DeleteModal } from "@components/modals";
import {DocsLink} from "@components/ExternalLink.tsx";
interface ListAddFormValues { interface ListAddFormValues {
name: string; name: string;
@ -729,6 +730,16 @@ function ListTypePlainText() {
label="List URL" label="List URL"
help="URL to a plain text file with one item per line" help="URL to a plain text file with one item per line"
placeholder="https://example.com/list.txt" placeholder="https://example.com/list.txt"
tooltip={
<div>
<p>Plaintext list can read from both http urls and local files on disk.</p>
<br />
<p>Remote: https://service.com/file.txt</p>
<br />
<p>Local: file:///home/username/file.txt</p>
<DocsLink href="https://autobrr.com/filters/lists" />
</div>
}
/> />
<div className="space-y-1"> <div className="space-y-1">