feat(outbounds): support custom subscription user agents (#6398)

Some subscription providers require a client-specific User-Agent before returning outbound links. Persist an optional value per subscription and use it for refreshes and previews while preserving the existing default for blank values.
This commit is contained in:
Timur Chernykh
2026-09-11 16:32:04 +03:00
committed by GitHub
parent 89ee1242bd
commit 9f07951ba7
22 changed files with 134 additions and 9 deletions
+14
View File
@@ -13569,6 +13569,11 @@
"type": "string", "type": "string",
"description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix." "description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix."
}, },
"userAgent": {
"type": "string",
"default": "3x-ui-outbound-sub/1.0",
"description": "Custom User-Agent sent when fetching this subscription. Defaults to \"3x-ui-outbound-sub/1.0\"."
},
"updateInterval": { "updateInterval": {
"type": "integer", "type": "integer",
"default": 600, "default": 600,
@@ -13662,6 +13667,11 @@
"type": "string", "type": "string",
"description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix." "description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix."
}, },
"userAgent": {
"type": "string",
"default": "3x-ui-outbound-sub/1.0",
"description": "Custom User-Agent sent when fetching this subscription. Defaults to \"3x-ui-outbound-sub/1.0\"."
},
"updateInterval": { "updateInterval": {
"type": "integer", "type": "integer",
"default": 600, "default": 600,
@@ -13914,6 +13924,10 @@
"type": "string", "type": "string",
"description": "Subscription URL to preview (required)." "description": "Subscription URL to preview (required)."
}, },
"userAgent": {
"type": "string",
"description": "Custom User-Agent sent while fetching the preview."
},
"allowPrivate": { "allowPrivate": {
"type": "boolean", "type": "boolean",
"description": "Allow a private/internal/loopback URL. Default false." "description": "Allow a private/internal/loopback URL. Default false."
+14
View File
@@ -13569,6 +13569,11 @@
"type": "string", "type": "string",
"description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix." "description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix."
}, },
"userAgent": {
"type": "string",
"default": "3x-ui-outbound-sub/1.0",
"description": "Custom User-Agent sent when fetching this subscription. Defaults to \"3x-ui-outbound-sub/1.0\"."
},
"updateInterval": { "updateInterval": {
"type": "integer", "type": "integer",
"default": 600, "default": 600,
@@ -13662,6 +13667,11 @@
"type": "string", "type": "string",
"description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix." "description": "Prefix for generated outbound tags. Defaults to the lowest free \"sub<N>-\" prefix."
}, },
"userAgent": {
"type": "string",
"default": "3x-ui-outbound-sub/1.0",
"description": "Custom User-Agent sent when fetching this subscription. Defaults to \"3x-ui-outbound-sub/1.0\"."
},
"updateInterval": { "updateInterval": {
"type": "integer", "type": "integer",
"default": 600, "default": 600,
@@ -13914,6 +13924,10 @@
"type": "string", "type": "string",
"description": "Subscription URL to preview (required)." "description": "Subscription URL to preview (required)."
}, },
"userAgent": {
"type": "string",
"description": "Custom User-Agent sent while fetching the preview."
},
"allowPrivate": { "allowPrivate": {
"type": "boolean", "type": "boolean",
"description": "Allow a private/internal/loopback URL. Default false." "description": "Allow a private/internal/loopback URL. Default false."
+15
View File
@@ -90,6 +90,14 @@ const outboundSubscriptionBodyParams: EndpointParam[] = [
desc: 'Prefix for generated outbound tags. Defaults to the lowest free "sub<N>-" prefix.', desc: 'Prefix for generated outbound tags. Defaults to the lowest free "sub<N>-" prefix.',
optional: true, optional: true,
}, },
{
name: 'userAgent',
in: 'body (form)',
type: 'string',
desc: 'Custom User-Agent sent when fetching this subscription. Defaults to "3x-ui-outbound-sub/1.0".',
optional: true,
defaultValue: '3x-ui-outbound-sub/1.0',
},
{ {
name: 'updateInterval', name: 'updateInterval',
in: 'body (form)', in: 'body (form)',
@@ -2502,6 +2510,13 @@ export const sections: readonly Section[] = [
type: 'string', type: 'string',
desc: 'Subscription URL to preview (required).', desc: 'Subscription URL to preview (required).',
}, },
{
name: 'userAgent',
in: 'body (form)',
type: 'string',
desc: 'Custom User-Agent sent while fetching the preview.',
optional: true,
},
{ {
name: 'allowPrivate', name: 'allowPrivate',
in: 'body (form)', in: 'body (form)',
@@ -62,6 +62,8 @@ import { useOutboundColumns } from './useOutboundColumns';
import OutboundCardList from './OutboundCardList'; import OutboundCardList from './OutboundCardList';
import SubscriptionOutbounds from './SubscriptionOutbounds'; import SubscriptionOutbounds from './SubscriptionOutbounds';
const defaultOutboundSubscriptionUserAgent = '3x-ui-outbound-sub/1.0';
interface OutboundSub { interface OutboundSub {
id: number; id: number;
remark?: string; remark?: string;
@@ -69,6 +71,7 @@ interface OutboundSub {
enabled?: boolean; enabled?: boolean;
allowPrivate?: boolean; allowPrivate?: boolean;
allowInsecure?: boolean; allowInsecure?: boolean;
userAgent?: string;
prepend?: boolean; prepend?: boolean;
priority?: number; priority?: number;
tagPrefix?: string; tagPrefix?: string;
@@ -136,6 +139,7 @@ export default function OutboundsTab({
remark: '', remark: '',
url: '', url: '',
tagPrefix: '', tagPrefix: '',
userAgent: '',
updateInterval: 600, updateInterval: 600,
enabled: true, enabled: true,
allowPrivate: false, allowPrivate: false,
@@ -334,6 +338,7 @@ export default function OutboundsTab({
remark?: string; remark?: string;
url?: string; url?: string;
tagPrefix?: string; tagPrefix?: string;
userAgent?: string;
updateInterval?: number; updateInterval?: number;
enabled?: boolean; enabled?: boolean;
allowPrivate?: boolean; allowPrivate?: boolean;
@@ -344,6 +349,7 @@ export default function OutboundsTab({
remark: src.remark ?? '', remark: src.remark ?? '',
url: src.url ?? '', url: src.url ?? '',
tagPrefix: src.tagPrefix ?? '', tagPrefix: src.tagPrefix ?? '',
userAgent: src.userAgent ?? '',
updateInterval: src.updateInterval ?? 600, updateInterval: src.updateInterval ?? 600,
enabled: src.enabled ?? true, enabled: src.enabled ?? true,
allowPrivate: src.allowPrivate ?? false, allowPrivate: src.allowPrivate ?? false,
@@ -356,6 +362,7 @@ export default function OutboundsTab({
remark: '', remark: '',
url: '', url: '',
tagPrefix: '', tagPrefix: '',
userAgent: '',
updateInterval: 600, updateInterval: 600,
enabled: true, enabled: true,
allowPrivate: false, allowPrivate: false,
@@ -370,6 +377,7 @@ export default function OutboundsTab({
remark: sub.remark ?? '', remark: sub.remark ?? '',
url: sub.url ?? '', url: sub.url ?? '',
tagPrefix: sub.tagPrefix ?? '', tagPrefix: sub.tagPrefix ?? '',
userAgent: sub.userAgent ?? '',
updateInterval: sub.updateInterval ?? 600, updateInterval: sub.updateInterval ?? 600,
enabled: sub.enabled ?? true, enabled: sub.enabled ?? true,
allowPrivate: sub.allowPrivate ?? false, allowPrivate: sub.allowPrivate ?? false,
@@ -423,7 +431,12 @@ export default function OutboundsTab({
try { try {
const r = await HttpUtil.post<{ tag?: string; protocol?: string }[]>( const r = await HttpUtil.post<{ tag?: string; protocol?: string }[]>(
'/panel/api/xray/outbound-subs/parse', '/panel/api/xray/outbound-subs/parse',
{ url: newSub.url, allowPrivate: newSub.allowPrivate }, {
url: newSub.url,
userAgent: newSub.userAgent,
allowPrivate: newSub.allowPrivate,
allowInsecure: newSub.allowInsecure,
},
); );
if (r?.success && Array.isArray(r.obj)) { if (r?.success && Array.isArray(r.obj)) {
setPreviewData(r.obj); setPreviewData(r.obj);
@@ -721,6 +734,13 @@ export default function OutboundsTab({
placeholder={t('pages.xray.outboundSub.tagPrefixPlaceholder')} placeholder={t('pages.xray.outboundSub.tagPrefixPlaceholder')}
/> />
</Form.Item> </Form.Item>
<Form.Item label={t('pages.xray.outboundSub.userAgent')}>
<Input
value={newSub.userAgent}
onChange={(e) => setNewSub({ ...newSub, userAgent: e.target.value })}
placeholder={defaultOutboundSubscriptionUserAgent}
/>
</Form.Item>
<Form.Item label={t('pages.xray.outboundSub.interval')}> <Form.Item label={t('pages.xray.outboundSub.interval')}>
<Space> <Space>
<InputNumber <InputNumber
+11
View File
@@ -96,10 +96,21 @@ func migrateClientTrafficLastSubFetchColumn() error {
return migrator.AddColumn(&xray.ClientTraffic{}, "LastSubFetch") return migrator.AddColumn(&xray.ClientTraffic{}, "LastSubFetch")
} }
func migrateOutboundSubscriptionUserAgentColumn() error {
migrator := db.Migrator()
if !migrator.HasTable(&model.OutboundSubscription{}) || migrator.HasColumn(&model.OutboundSubscription{}, "user_agent") {
return nil
}
return migrator.AddColumn(&model.OutboundSubscription{}, "UserAgent")
}
func initModels() error { func initModels() error {
if err := migrateClientTrafficLastSubFetchColumn(); err != nil { if err := migrateClientTrafficLastSubFetchColumn(); err != nil {
return err return err
} }
if err := migrateOutboundSubscriptionUserAgentColumn(); err != nil {
return err
}
models := allModels() models := allModels()
for _, mdl := range models { for _, mdl := range models {
if IsPostgres() && postgresModelSettled(mdl) { if IsPostgres() && postgresModelSettled(mdl) {
+1
View File
@@ -1248,6 +1248,7 @@ type OutboundSubscription struct {
Enabled bool `json:"enabled" form:"enabled" gorm:"default:true"` Enabled bool `json:"enabled" form:"enabled" gorm:"default:true"`
AllowPrivate bool `json:"allowPrivate" form:"allowPrivate" gorm:"default:false"` AllowPrivate bool `json:"allowPrivate" form:"allowPrivate" gorm:"default:false"`
AllowInsecure bool `json:"allowInsecure" form:"allowInsecure" gorm:"default:false"` AllowInsecure bool `json:"allowInsecure" form:"allowInsecure" gorm:"default:false"`
UserAgent string `json:"userAgent" form:"userAgent"`
TagPrefix string `json:"tagPrefix" form:"tagPrefix"` TagPrefix string `json:"tagPrefix" form:"tagPrefix"`
UpdateInterval int `json:"updateInterval" form:"updateInterval" gorm:"default:600"` // seconds between refreshes UpdateInterval int `json:"updateInterval" form:"updateInterval" gorm:"default:600"` // seconds between refreshes
Priority int `json:"priority" form:"priority" gorm:"default:0"` // order among subscriptions in the merged outbounds (lower = earlier) Priority int `json:"priority" form:"priority" gorm:"default:0"` // order among subscriptions in the merged outbounds (lower = earlier)
+6 -3
View File
@@ -516,6 +516,7 @@ func (a *XraySettingController) createOutboundSub(c *gin.Context) {
remark := c.PostForm("remark") remark := c.PostForm("remark")
rawURL := c.PostForm("url") rawURL := c.PostForm("url")
prefix := c.PostForm("tagPrefix") prefix := c.PostForm("tagPrefix")
userAgent := c.PostForm("userAgent")
enabled := c.PostForm("enabled") != "false" enabled := c.PostForm("enabled") != "false"
allowPrivate := c.PostForm("allowPrivate") == "true" allowPrivate := c.PostForm("allowPrivate") == "true"
allowInsecure := c.PostForm("allowInsecure") == "true" allowInsecure := c.PostForm("allowInsecure") == "true"
@@ -527,7 +528,7 @@ func (a *XraySettingController) createOutboundSub(c *gin.Context) {
interval = v interval = v
} }
} }
sub, err := a.OutboundSubscriptionService.Create(remark, rawURL, prefix, enabled, interval, allowPrivate, prepend, allowInsecure) sub, err := a.OutboundSubscriptionService.Create(remark, rawURL, prefix, userAgent, enabled, interval, allowPrivate, prepend, allowInsecure)
if err != nil { if err != nil {
jsonMsg(c, "Failed to create outbound subscription", err) jsonMsg(c, "Failed to create outbound subscription", err)
return return
@@ -545,6 +546,7 @@ func (a *XraySettingController) updateOutboundSub(c *gin.Context) {
remark := c.PostForm("remark") remark := c.PostForm("remark")
rawURL := c.PostForm("url") rawURL := c.PostForm("url")
prefix := c.PostForm("tagPrefix") prefix := c.PostForm("tagPrefix")
userAgent := c.PostForm("userAgent")
enabled := c.PostForm("enabled") != "false" enabled := c.PostForm("enabled") != "false"
allowPrivate := c.PostForm("allowPrivate") == "true" allowPrivate := c.PostForm("allowPrivate") == "true"
allowInsecure := c.PostForm("allowInsecure") == "true" allowInsecure := c.PostForm("allowInsecure") == "true"
@@ -556,7 +558,7 @@ func (a *XraySettingController) updateOutboundSub(c *gin.Context) {
interval = v interval = v
} }
} }
if err := a.OutboundSubscriptionService.Update(subID, remark, rawURL, prefix, enabled, interval, allowPrivate, prepend, allowInsecure); err != nil { if err := a.OutboundSubscriptionService.Update(subID, remark, rawURL, prefix, userAgent, enabled, interval, allowPrivate, prepend, allowInsecure); err != nil {
jsonMsg(c, "Failed to update outbound subscription", err) jsonMsg(c, "Failed to update outbound subscription", err)
return return
} }
@@ -624,12 +626,13 @@ func (a *XraySettingController) parseOutboundSubURL(c *gin.Context) {
} }
allowPrivate := c.PostForm("allowPrivate") == "true" allowPrivate := c.PostForm("allowPrivate") == "true"
allowInsecure := c.PostForm("allowInsecure") == "true" allowInsecure := c.PostForm("allowInsecure") == "true"
userAgent := c.PostForm("userAgent")
// Use a throw-away service instance; it only needs the settingService for proxy. // Use a throw-away service instance; it only needs the settingService for proxy.
svc := service.OutboundSubscriptionService{} svc := service.OutboundSubscriptionService{}
// We don't have a direct "fetch once" that returns without storing, so we // We don't have a direct "fetch once" that returns without storing, so we
// temporarily create a disabled row, refresh it, then delete. Cleaner would // temporarily create a disabled row, refresh it, then delete. Cleaner would
// be to expose a pure ParseURL on the service, but this keeps the surface small. // be to expose a pure ParseURL on the service, but this keeps the surface small.
tmp, err := svc.Create("preview", rawURL, "", false, 600, allowPrivate, false, allowInsecure) tmp, err := svc.Create("preview", rawURL, "", userAgent, false, 600, allowPrivate, false, allowInsecure)
if err != nil { if err != nil {
jsonMsg(c, "Failed to preview subscription", err) jsonMsg(c, "Failed to preview subscription", err)
return return
+11 -3
View File
@@ -58,6 +58,8 @@ func filterOutboundsRejectedByCore(label string, outbounds []any) ([]any, []stri
// subscription may aggregate many upstream outbounds into one document. // subscription may aggregate many upstream outbounds into one document.
const maxOutboundSubscriptionBytes int64 = 8 << 20 const maxOutboundSubscriptionBytes int64 = 8 << 20
const defaultOutboundSubscriptionUserAgent = "3x-ui-outbound-sub/1.0"
var errOutboundSubscriptionBodyTooLarge = errors.New("outbound subscription response body exceeds size limit") var errOutboundSubscriptionBodyTooLarge = errors.New("outbound subscription response body exceeds size limit")
func readBoundedOutboundSubscriptionBody(r io.Reader) ([]byte, error) { func readBoundedOutboundSubscriptionBody(r io.Reader) ([]byte, error) {
@@ -164,7 +166,7 @@ func (s *OutboundSubscriptionService) nextDefaultSubPrefix(excludeId int) (strin
return fmt.Sprintf("sub%d-", defaultPrefixNumber(subs, excludeId)), nil return fmt.Sprintf("sub%d-", defaultPrefixNumber(subs, excludeId)), nil
} }
func (s *OutboundSubscriptionService) Create(remark, rawURL, tagPrefix string, enabled bool, updateInterval int, allowPrivate, prepend, allowInsecure bool) (*model.OutboundSubscription, error) { func (s *OutboundSubscriptionService) Create(remark, rawURL, tagPrefix, userAgent string, enabled bool, updateInterval int, allowPrivate, prepend, allowInsecure bool) (*model.OutboundSubscription, error) {
cleanURL, err := SanitizePublicHTTPURL(rawURL, allowPrivate) cleanURL, err := SanitizePublicHTTPURL(rawURL, allowPrivate)
if err != nil { if err != nil {
return nil, common.NewError("invalid subscription URL:", err) return nil, common.NewError("invalid subscription URL:", err)
@@ -193,6 +195,7 @@ func (s *OutboundSubscriptionService) Create(remark, rawURL, tagPrefix string, e
Enabled: enabled, Enabled: enabled,
AllowPrivate: allowPrivate, AllowPrivate: allowPrivate,
AllowInsecure: allowInsecure, AllowInsecure: allowInsecure,
UserAgent: strings.TrimSpace(userAgent),
Prepend: prepend, Prepend: prepend,
Priority: int(count), Priority: int(count),
TagPrefix: prefix, TagPrefix: prefix,
@@ -205,7 +208,7 @@ func (s *OutboundSubscriptionService) Create(remark, rawURL, tagPrefix string, e
} }
// Update updates editable fields. // Update updates editable fields.
func (s *OutboundSubscriptionService) Update(id int, remark, rawURL, tagPrefix string, enabled bool, updateInterval int, allowPrivate, prepend, allowInsecure bool) error { func (s *OutboundSubscriptionService) Update(id int, remark, rawURL, tagPrefix, userAgent string, enabled bool, updateInterval int, allowPrivate, prepend, allowInsecure bool) error {
sub, err := s.Get(id) sub, err := s.Get(id)
if err != nil { if err != nil {
return err return err
@@ -232,6 +235,7 @@ func (s *OutboundSubscriptionService) Update(id int, remark, rawURL, tagPrefix s
sub.Enabled = enabled sub.Enabled = enabled
sub.AllowPrivate = allowPrivate sub.AllowPrivate = allowPrivate
sub.AllowInsecure = allowInsecure sub.AllowInsecure = allowInsecure
sub.UserAgent = strings.TrimSpace(userAgent)
sub.Prepend = prepend sub.Prepend = prepend
sub.TagPrefix = prefix sub.TagPrefix = prefix
sub.UpdateInterval = updateInterval sub.UpdateInterval = updateInterval
@@ -363,7 +367,11 @@ func (s *OutboundSubscriptionService) fetchAndStore(sub *model.OutboundSubscript
s.recordError(sub, err) s.recordError(sub, err)
return nil, err return nil, err
} }
req.Header.Set("User-Agent", "3x-ui-outbound-sub/1.0") userAgent := strings.TrimSpace(sub.UserAgent)
if userAgent == "" {
userAgent = defaultOutboundSubscriptionUserAgent
}
req.Header.Set("User-Agent", userAgent)
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
@@ -3,6 +3,8 @@ package service
import ( import (
"bytes" "bytes"
"errors" "errors"
"net/http"
"net/http/httptest"
"slices" "slices"
"testing" "testing"
@@ -40,7 +42,7 @@ func TestOutboundSubscriptionCreatePropagatesAllocationDatabaseFailures(t *testi
{name: "priority count query", tagPrefix: "custom-", operation: "priority allocation"}, {name: "priority count query", tagPrefix: "custom-", operation: "priority allocation"},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
created, err := (&OutboundSubscriptionService{}).Create("test", "https://1.1.1.1/sub", tc.tagPrefix, true, 600, false, false, false) created, err := (&OutboundSubscriptionService{}).Create("test", "https://1.1.1.1/sub", tc.tagPrefix, "", true, 600, false, false, false)
if !errors.Is(err, errInjected) { if !errors.Is(err, errInjected) {
t.Fatalf("Create error = %v, want injected %s query failure", err, tc.operation) t.Fatalf("Create error = %v, want injected %s query failure", err, tc.operation)
} }
@@ -83,7 +85,7 @@ func TestOutboundSubscriptionUpdatePropagatesPrefixQueryFailureWithoutMutation(t
}) })
err := (&OutboundSubscriptionService{}).Update( err := (&OutboundSubscriptionService{}).Update(
original.Id, "after", "https://1.1.1.1/changed", "", false, 1200, false, false, false, original.Id, "after", "https://1.1.1.1/changed", "", "", false, 1200, false, false, false,
) )
if !errors.Is(err, errInjected) { if !errors.Is(err, errInjected) {
t.Fatalf("Update error = %v, want injected prefix query failure", err) t.Fatalf("Update error = %v, want injected prefix query failure", err)
@@ -102,6 +104,30 @@ func TestOutboundSubscriptionUpdatePropagatesPrefixQueryFailureWithoutMutation(t
} }
} }
func TestOutboundSubscriptionRefreshUsesCustomUserAgent(t *testing.T) {
setupSettingTestDB(t)
const wantUserAgent = "ClashMetaForAndroid/2.11.13"
var gotUserAgent string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotUserAgent = r.UserAgent()
_, _ = w.Write([]byte("vless://00000000-0000-4000-8000-000000000000@1.1.1.1:443?security=tls&type=tcp#node"))
}))
t.Cleanup(server.Close)
sub := &model.OutboundSubscription{
Url: server.URL, AllowPrivate: true, UserAgent: wantUserAgent, TagPrefix: "test-",
}
if err := database.GetDB().Create(sub).Error; err != nil {
t.Fatalf("seed subscription: %v", err)
}
if _, err := (&OutboundSubscriptionService{}).Refresh(sub.Id); err != nil {
t.Fatalf("Refresh: %v", err)
}
if gotUserAgent != wantUserAgent {
t.Fatalf("User-Agent = %q, want %q", gotUserAgent, wantUserAgent)
}
}
func TestReadBoundedOutboundSubscriptionBody(t *testing.T) { func TestReadBoundedOutboundSubscriptionBody(t *testing.T) {
t.Run("accepts body at the limit", func(t *testing.T) { t.Run("accepts body at the limit", func(t *testing.T) {
want := bytes.Repeat([]byte("a"), int(maxOutboundSubscriptionBytes)) want := bytes.Repeat([]byte("a"), int(maxOutboundSubscriptionBytes))
+1
View File
@@ -1788,6 +1788,7 @@
"urlPlaceholder": "https://... (قائمة روابط بصيغة base64)", "urlPlaceholder": "https://... (قائمة روابط بصيغة base64)",
"tagPrefix": "بادئة الوسم", "tagPrefix": "بادئة الوسم",
"tagPrefixPlaceholder": "hk-", "tagPrefixPlaceholder": "hk-",
"userAgent": "User-Agent",
"interval": "فاصل التحديث", "interval": "فاصل التحديث",
"hours": "س", "hours": "س",
"minutes": "د", "minutes": "د",
+1
View File
@@ -1906,6 +1906,7 @@
"urlPlaceholder": "https://... (base64 list of links)", "urlPlaceholder": "https://... (base64 list of links)",
"tagPrefix": "Tag prefix", "tagPrefix": "Tag prefix",
"tagPrefixPlaceholder": "hk-", "tagPrefixPlaceholder": "hk-",
"userAgent": "User-Agent",
"interval": "Update interval", "interval": "Update interval",
"hours": "h", "hours": "h",
"minutes": "min", "minutes": "min",
+1
View File
@@ -1788,6 +1788,7 @@
"urlPlaceholder": "https://... (lista de enlaces en base64)", "urlPlaceholder": "https://... (lista de enlaces en base64)",
"tagPrefix": "Prefijo de etiqueta", "tagPrefix": "Prefijo de etiqueta",
"tagPrefixPlaceholder": "hk-", "tagPrefixPlaceholder": "hk-",
"userAgent": "User-Agent",
"interval": "Intervalo de actualización", "interval": "Intervalo de actualización",
"hours": "h", "hours": "h",
"minutes": "min", "minutes": "min",
+1
View File
@@ -1788,6 +1788,7 @@
"urlPlaceholder": "https://... (فهرست base64 از لینک‌ها)", "urlPlaceholder": "https://... (فهرست base64 از لینک‌ها)",
"tagPrefix": "پیشوند تگ", "tagPrefix": "پیشوند تگ",
"tagPrefixPlaceholder": "hk-", "tagPrefixPlaceholder": "hk-",
"userAgent": "User-Agent",
"interval": "بازه به‌روزرسانی", "interval": "بازه به‌روزرسانی",
"hours": "ساعت", "hours": "ساعت",
"minutes": "دقیقه", "minutes": "دقیقه",
+1
View File
@@ -1788,6 +1788,7 @@
"urlPlaceholder": "https://... (daftar tautan base64)", "urlPlaceholder": "https://... (daftar tautan base64)",
"tagPrefix": "Awalan tag", "tagPrefix": "Awalan tag",
"tagPrefixPlaceholder": "hk-", "tagPrefixPlaceholder": "hk-",
"userAgent": "User-Agent",
"interval": "Interval pembaruan", "interval": "Interval pembaruan",
"hours": "j", "hours": "j",
"minutes": "mnt", "minutes": "mnt",
+1
View File
@@ -1788,6 +1788,7 @@
"urlPlaceholder": "https://...(リンクのbase64リスト)", "urlPlaceholder": "https://...(リンクのbase64リスト)",
"tagPrefix": "タグのプレフィックス", "tagPrefix": "タグのプレフィックス",
"tagPrefixPlaceholder": "hk-", "tagPrefixPlaceholder": "hk-",
"userAgent": "User-Agent",
"interval": "更新間隔", "interval": "更新間隔",
"hours": "時間", "hours": "時間",
"minutes": "分", "minutes": "分",
+1
View File
@@ -1788,6 +1788,7 @@
"urlPlaceholder": "https://... (lista de links em base64)", "urlPlaceholder": "https://... (lista de links em base64)",
"tagPrefix": "Prefixo da tag", "tagPrefix": "Prefixo da tag",
"tagPrefixPlaceholder": "hk-", "tagPrefixPlaceholder": "hk-",
"userAgent": "User-Agent",
"interval": "Intervalo de atualização", "interval": "Intervalo de atualização",
"hours": "h", "hours": "h",
"minutes": "min", "minutes": "min",
+1
View File
@@ -1788,6 +1788,7 @@
"urlPlaceholder": "https://... (список ссылок в base64)", "urlPlaceholder": "https://... (список ссылок в base64)",
"tagPrefix": "Префикс тега", "tagPrefix": "Префикс тега",
"tagPrefixPlaceholder": "hk-", "tagPrefixPlaceholder": "hk-",
"userAgent": "User-Agent",
"interval": "Интервал обновления", "interval": "Интервал обновления",
"hours": "ч", "hours": "ч",
"minutes": "мин", "minutes": "мин",
+1
View File
@@ -1788,6 +1788,7 @@
"urlPlaceholder": "https://... (bağlantıların base64 listesi)", "urlPlaceholder": "https://... (bağlantıların base64 listesi)",
"tagPrefix": "Etiket öneki", "tagPrefix": "Etiket öneki",
"tagPrefixPlaceholder": "hk-", "tagPrefixPlaceholder": "hk-",
"userAgent": "User-Agent",
"interval": "Güncelleme aralığı", "interval": "Güncelleme aralığı",
"hours": "sa", "hours": "sa",
"minutes": "dk", "minutes": "dk",
+1
View File
@@ -1788,6 +1788,7 @@
"urlPlaceholder": "https://... (список посилань у base64)", "urlPlaceholder": "https://... (список посилань у base64)",
"tagPrefix": "Префікс тегу", "tagPrefix": "Префікс тегу",
"tagPrefixPlaceholder": "hk-", "tagPrefixPlaceholder": "hk-",
"userAgent": "User-Agent",
"interval": "Інтервал оновлення", "interval": "Інтервал оновлення",
"hours": "год", "hours": "год",
"minutes": "хв", "minutes": "хв",
+1
View File
@@ -1788,6 +1788,7 @@
"urlPlaceholder": "https://... (danh sách liên kết base64)", "urlPlaceholder": "https://... (danh sách liên kết base64)",
"tagPrefix": "Tiền tố tag", "tagPrefix": "Tiền tố tag",
"tagPrefixPlaceholder": "hk-", "tagPrefixPlaceholder": "hk-",
"userAgent": "User-Agent",
"interval": "Khoảng cập nhật", "interval": "Khoảng cập nhật",
"hours": "giờ", "hours": "giờ",
"minutes": "phút", "minutes": "phút",
+1
View File
@@ -1788,6 +1788,7 @@
"urlPlaceholder": "https://...base64 编码的链接列表)", "urlPlaceholder": "https://...base64 编码的链接列表)",
"tagPrefix": "标签前缀", "tagPrefix": "标签前缀",
"tagPrefixPlaceholder": "hk-", "tagPrefixPlaceholder": "hk-",
"userAgent": "User-Agent",
"interval": "更新间隔", "interval": "更新间隔",
"hours": "时", "hours": "时",
"minutes": "分", "minutes": "分",
+1
View File
@@ -1788,6 +1788,7 @@
"urlPlaceholder": "https://...base64 連結清單)", "urlPlaceholder": "https://...base64 連結清單)",
"tagPrefix": "標籤前綴", "tagPrefix": "標籤前綴",
"tagPrefixPlaceholder": "hk-", "tagPrefixPlaceholder": "hk-",
"userAgent": "User-Agent",
"interval": "更新間隔", "interval": "更新間隔",
"hours": "時", "hours": "時",
"minutes": "分", "minutes": "分",