feature: add global.torrent settings card

feature: remember make torrent options
pull/3671/head
chalkim 2024-06-21 02:28:47 +08:00
parent 9ff78ded0e
commit 1d1cc4b7f8
24 changed files with 476 additions and 231 deletions

View File

@ -17,4 +17,4 @@ EXPOSE 80
COPY docker_config.json /.filebrowser.json
COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ]
ENTRYPOINT [ "/filebrowser" ]

View File

@ -14,4 +14,4 @@ COPY filebrowser /usr/bin/filebrowser
# ports and volumes
VOLUME /srv /config /database
EXPOSE 80
EXPOSE 80

View File

@ -337,6 +337,9 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
Download: true,
Torrent: true,
},
CreateBody: users.CreateTorrentBody{
Date: true,
},
},
AuthMethod: "",
Branding: settings.Branding{},

View File

@ -78,7 +78,7 @@ func addUserFlags(flags *pflag.FlagSet) {
flags.String("locale", "en", "locale for users")
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
flags.Bool("singleClick", false, "use single clicks only")
flags.String("trackerListsUrl", "https://cf.trackerslist.com/all.txt", "tracker lists url")
flags.String("trackersListUrl", "https://cf.trackerslist.com/all.txt", "tracker lists url")
}
func getViewMode(flags *pflag.FlagSet) users.ViewMode {

View File

@ -48,6 +48,7 @@ options you want to change.`,
Perm: user.Perm,
Sorting: user.Sorting,
Commands: user.Commands,
CreateBody: user.CreateBody,
}
getUserDefaults(flags, &defaults, false)
user.Scope = defaults.Scope
@ -57,6 +58,7 @@ options you want to change.`,
user.Perm = defaults.Perm
user.Commands = defaults.Commands
user.Sorting = defaults.Sorting
user.CreateBody = defaults.CreateBody
user.LockPassword = mustGetBool(flags, "lockPassword")
if newUsername != "" {

View File

@ -1,5 +1,10 @@
import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
export async function fetchDefaultOptions() {
const url = `/api/torrent`
return fetchJSON(url);
}
export async function makeTorrent(
url: string,
announces: string[],

View File

@ -2,7 +2,7 @@
<div class="card floating">
<div class="card-content">
<p>
{{ $t("prompts.publishMessageSingle") }}
{{ $t("prompts.publishMessage") }}
</p>
</div>
<div class="card-action">
@ -65,9 +65,9 @@ export default {
this.req.items[this.selected[0]].url
).then(() => {
buttons.success("publish");
this.$showSuccess(this.$t("prompts.publishSuccess"));
this.$showSuccess(this.$t("success.torrentPublished"));
}).catch((e) => {
buttons.done("delete");
buttons.done("publish");
this.$showError(e);
});
};

View File

@ -2,44 +2,85 @@
<div class="card floating" style="max-width: 50em;" id="share">
<div class="card-title">
<h2>{{ $t("buttons.torrent") }}</h2>
<button class="button button--flat button--blue" @click="toggleDetailedView">
{{ detailedView ? $t("buttons.compact") : $t("buttons.detailed") }}
</button>
</div>
<div class="card-content">
<!--
<p>{{ $t("prompts.name") }}</p>
<input
class="input input--block"
type="text"
v-model.trim="name"
tabindex="0" />
-->
<p v-if="detailedView">{{ $t("prompts.pieceLength") }}</p>
<select
v-if="detailedView"
class="input input--block"
v-model.trim="pieceLen"
tabindex="1">
<option value="0">{{ $t("prompts.auto") }}</option>
<option value="15">32 KiB</option>
<option value="16">64 KiB</option>
<option value="17">128 KiB</option>
<option value="18">256 KiB</option>
<option value="19">512 KiB</option>
<option value="20">1 MiB</option>
<option value="21">2 MiB</option>
<option value="22">4 MiB</option>
<option value="23">8 MiB</option>
<option value="24">16 MiB</option>
<option value="25">32 MiB</option>
<option value="26">64 MiB</option>
<option value="27">128 MiB</option>
<option value="28">256 MiB</option>
</select>
<p>{{ $t("prompts.trackersList") }}</p>
<textarea
class="input input--block input--textarea"
style="min-height: 5em;"
style="min-height: 4em;"
type="text"
v-model.trim="announces"
tabindex="1"></textarea>
tabindex="2"></textarea>
<p>{{ $t("prompts.webSeeds") }}</p>
<textarea
<textarea
class="input input--block input--textarea"
style="min-height: 5em;"
style="min-height: 4em;"
type="text"
v-model.trim="webSeeds"
tabindex="2"></textarea>
tabindex="3"></textarea>
<p>{{ $t("prompts.comment") }}</p>
<textarea
class="input input--block input--textarea"
style="min-height: 5em;"
style="min-height: 4em;"
type="text"
v-model.trim="comment"
tabindex="3"></textarea>
tabindex="4"></textarea>
<p>{{ $t("prompts.source") }}</p>
<p v-if="detailedView">{{ $t("prompts.source") }}</p>
<input
v-if="detailedView"
class="input input--block"
type="text"
v-model.trim="source"
tabindex="4" />
tabindex="5" />
<label>
<input type="checkbox" v-model="privateFlag" tabindex="3" />
<p v-if="detailedView">
<input type="checkbox" v-model="date" tabindex="6" />
{{ $t("prompts.includeDate") }}
</p>
<p v-if="detailedView">
<input type="checkbox" v-model="privateFlag" tabindex="7" />
{{ $t("prompts.privateTorrent") }}
</label>
</p>
</div>
<div class="card-action">
@ -70,10 +111,11 @@ export default {
comment: "",
date: true,
name: "",
pieceLen: 18,
pieceLen: 0,
privateFlag: false,
source: "",
webSeeds: [],
detailedView: false
};
},
inject: ["$showError", "$showSuccess"],
@ -99,25 +141,21 @@ export default {
},
},
async beforeMount() {
this.fetchTrackers();
api.fetchDefaultOptions().then((res) => {
this.announces = res.announces.join("\n");
this.comment = res.comment;
this.date = res.date;
this.name = res.name;
this.pieceLen = res.pieceLen;
this.privateFlag = res.private;
this.source = res.source;
this.webSeeds = res.webSeeds.join("\n");
});
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
async fetchTrackers() {
const url = 'https://cf.trackerslist.com/all.txt';
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(this.$t("error.failedToFetchTrackers"));
}
let text = await response.text();
text = text.replace(/^\s*[\r\n]/gm, '');
this.announces = text;
} catch (error) {
this.$showError(error);
text = this.$t("error.faildToFetchTrackers");
this.announces = text;
}
toggleDetailedView() {
this.detailedView = !this.detailedView;
},
torrent: async function (event) {
event.preventDefault();
@ -135,10 +173,10 @@ export default {
this.comment,
this.date,
this.name,
this.pieceLen,
parseInt(this.pieceLen),
this.privateFlag,
this.source,
this.webSeeds,
this.webSeeds.split("\n").map((t) => t.trim()).filter((t) => t),
).then(
() => {
buttons.success("torrent");
@ -147,9 +185,9 @@ export default {
this.$showSuccess(this.$t("success.torrentCreated"));
}
).catch((e) => {
buttons.done("torrent");
this.$showError(e);
});
buttons.done("torrent");
this.$showError(e);
});
}
this.closeHovers();

View File

@ -42,7 +42,9 @@
"openFile": "打开文件",
"continue": "继续",
"fullScreen": "切换全屏",
"discardChanges": "放弃更改"
"discardChanges": "放弃更改",
"detailed": "转到详细视图",
"compact": "转到精简视图"
},
"download": {
"downloadFile": "下载文件",
@ -144,11 +146,15 @@
"resolution": "分辨率",
"deleteUser": "你确定要删除这个用户吗?",
"discardEditorChanges": "你确定要放弃所做的更改吗?",
"pieceLength": "分片大小:",
"auto": "自动",
"trackersList": "Tracker URL",
"comment": "注释:",
"webSeeds": "Web 种子 URL",
"source": "源:",
"privateTorrent": "私有torrent不会在DHT网络上分发"
"includeDate": "写入创建日期",
"privateTorrent": "私有 torrent 不会在DHT网络上分发",
"publishMessage": "你确定要发布这个种子吗?"
},
"search": {
"images": "图像",
@ -245,7 +251,16 @@
"userManagement": "用户管理",
"userUpdated": "用户已更新!",
"username": "用户名",
"users": "用户"
"users": "用户",
"torrentSettings": "BT设置",
"makeTorrent": "制种",
"makeTorrentSettingsDescription": "你可以在此设置BT种子*.torrent的默认参数。这些参数可以在创建种子时修改。",
"trackersListUrl": "Tracker URL 列表来源",
"publish": "发布",
"qbSettingsDescription": "你可以在此设置 qBittorrent WebUI 的地址、用户名和密码。",
"qbUrl": "qBittorrent WebUI 地址",
"qbUsername": "qBittorrent WebUI 用户名",
"qbPassword": "qBittorrent WebUI 密码"
},
"sidebar": {
"help": "帮助",
@ -262,7 +277,8 @@
},
"success": {
"linkCopied": "链接已复制!",
"torrentCreated": "种子已创建!"
"torrentCreated": "种子已创建!",
"torrentPublished": "种子已发布!"
},
"time": {
"days": "天",

View File

@ -8,6 +8,7 @@ interface ISettings {
tus: SettingsTus;
shell: string[];
commands: SettingsCommand;
torrent: SettingsTorrent;
}
interface SettingsDefaults {
@ -49,6 +50,13 @@ interface SettingsCommand {
before_upload?: string[];
}
interface SettingsTorrent {
trackersListUrl?: string;
qbUrl?: string;
qbUsername?: string;
qbPassword?: string;
}
interface SettingsUnit {
KB: number;
MB: number;

View File

@ -225,6 +225,67 @@
</div>
</form>
</div>
<div class="column">
<form class="card" @submit.prevent="save">
<div class="card-title">
<h2>{{ t("settings.torrentSettings") }}</h2>
</div>
<div class="card-content">
<h3>{{ t("settings.makeTorrent") }}</h3>
<p class="small">{{ t("settings.makeTorrentSettingsDescription") }}</p>
<p>
<label for="trackersListUrl">{{ t("settings.trackersListUrl") }}</label>
<input
class="input input--block"
type="text"
v-model="settings.torrent.trackersListUrl"
id="trackersListUrl"
/>
</p>
<h3>{{ t("settings.publish") }}</h3>
<p class="small">{{ t("settings.qbSettingsDescription") }}</p>
<p>
<label for="qbUrl">{{ t("settings.qbUrl") }}</label>
<input
class="input input--block"
v-model.trim="settings.torrent.qbUrl"
id="qbUrl"
></input>
</p>
<p>
<label for="qbUsername">{{ t("settings.qbUsername") }}</label>
<input
class="input input--block"
v-model.trim="settings.torrent.qbUsername"
id="qbUsername"
></input>
</p>
<p>
<label for="qbPassword">{{ t("settings.qbPassword") }}</label>
<input
class="input input--block"
type="password"
placeholder="●●●●●●●●"
v-model.trim="settings.torrent.qbPassword"
id="qbPassword"
></input>
</p>
</div>
<div class="card-action">
<input
class="button button--flat"
type="submit"
:value="t('buttons.update')"
/>
</div>
</form>
</div>
</div>
</template>

View File

@ -11,6 +11,7 @@ import (
"github.com/filebrowser/filebrowser/v2/runner"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/torrent"
"github.com/filebrowser/filebrowser/v2/users"
)
@ -18,6 +19,7 @@ type handleFunc func(w http.ResponseWriter, r *http.Request, d *data) (int, erro
type data struct {
*runner.Runner
*torrent.Torrent
settings *settings.Settings
server *settings.Server
store *storage.Storage
@ -61,6 +63,7 @@ func handle(fn handleFunc, prefix string, store *storage.Storage, server *settin
status, err := fn(w, r, &data{
Runner: &runner.Runner{Enabled: server.EnableExec, Settings: settings},
Torrent: &torrent.Torrent{Settings: settings},
store: store,
settings: settings,
server: server,

View File

@ -78,6 +78,7 @@ func NewHandler(
api.PathPrefix("/share").Handler(monkey(sharePostHandler, "/api/share")).Methods("POST")
api.PathPrefix("/share").Handler(monkey(shareDeleteHandler, "/api/share")).Methods("DELETE")
api.PathPrefix("/torrent").Handler(monkey(torrentGetHandler, "/api/torrent")).Methods("Get")
api.PathPrefix("/torrent").Handler(monkey(torrentPostHandler, "/api/torrent")).Methods("POST")
api.PathPrefix("/publish").Handler(monkey(publishPostHandler, "/api/publish")).Methods("POST")

View File

@ -18,10 +18,11 @@ type settingsData struct {
Tus settings.Tus `json:"tus"`
Shell []string `json:"shell"`
Commands map[string][]string `json:"commands"`
Torrent settings.Torrent `json:"torrent"`
}
var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
data := &settingsData{
data := settingsData{
Signup: d.settings.Signup,
CreateUserDir: d.settings.CreateUserDir,
UserHomeBasePath: d.settings.UserHomeBasePath,
@ -31,8 +32,11 @@ var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request,
Tus: d.settings.Tus,
Shell: d.settings.Shell,
Commands: d.settings.Commands,
Torrent: d.settings.Torrent,
}
data.Torrent.QbPassword = ""
return renderJSON(w, r, data)
})
@ -52,6 +56,7 @@ var settingsPutHandler = withAdmin(func(_ http.ResponseWriter, r *http.Request,
d.settings.Tus = req.Tus
d.settings.Shell = req.Shell
d.settings.Commands = req.Commands
d.settings.Torrent = req.Torrent
err = d.store.Settings.Save(d.settings)
return errToStatus(err), err

View File

@ -4,11 +4,10 @@ import (
"encoding/json"
"fmt"
"net/http"
"os/exec"
"path/filepath"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/torrent"
"github.com/filebrowser/filebrowser/v2/users"
)
func withPermTorrent(fn handleFunc) handleFunc {
@ -21,97 +20,15 @@ func withPermTorrent(fn handleFunc) handleFunc {
})
}
/*
mktorrent 1.1 (c) 2007, 2009 Emil Renner Berthing
Usage: mktorrent [OPTIONS] <target directory or filename>
Options:
-a, --announce=<url>[,<url>]* : specify the full announce URLs
at least one is required
additional -a adds backup trackers
-c, --comment=<comment> : add a comment to the metainfo
-d, --no-date : don't write the creation date
-h, --help : show this help screen
-l, --piece-length=<n> : set the piece length to 2^n bytes,
default is 18, that is 2^18 = 256kb
-n, --name=<name> : set the name of the torrent
default is the basename of the target
-o, --output=<filename> : set the path and filename of the created file
default is <name>.torrent
-p, --private : set the private flag
-s, --source=<source> : add source string embedded in infohash
-t, --threads=<n> : use <n> threads for calculating hashes
default is the number of CPU cores
-v, --verbose : be verbose
-w, --web-seed=<url>[,<url>]* : add web seed URLs
additional -w adds more URLs
*/
// TorrentOptions 结构体定义了 mktorrent 命令的选项
type TorrentOptions struct {
Target string // 目标文件或目录路径
Name string // 种子名称
OutputFile string // 输出种子文件的路径和文件名
Announces []string // 主要的 tracker URLs
Comment string // 添加的注释
Date bool // 是否写入创建日期
PieceLen int // 设置块大小
Private bool // 是否设置为私有种子
Source string // 添加到 infohash 中的源字符串
Threads int // 使用的线程数
WebSeeds []string // Web seed URLs
}
// buildArgs 函数根据 TorrentOptions 构建 mktorrent 命令的参数列表
func buildArgs(opts TorrentOptions) []string {
args := []string{}
for _, announce := range opts.Announces {
args = append(args, "-a", announce)
var torrentGetHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
// return default torrent options
s, err := d.GetDefaultCreateBody(d.user.CreateBody)
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to get default create body: %w", err)
}
if opts.Comment != "" {
args = append(args, "-c", opts.Comment)
}
if !opts.Date {
args = append(args, "-d")
}
if opts.PieceLen > 0 {
args = append(args, "-l", fmt.Sprintf("%d", opts.PieceLen))
}
if opts.Name != "" {
args = append(args, "-n", opts.Name)
}
if opts.OutputFile != "" {
args = append(args, "-o", opts.OutputFile)
}
if opts.Private {
args = append(args, "-p")
}
if opts.Source != "" {
args = append(args, "-s", opts.Source)
}
if opts.Threads > 0 {
args = append(args, "-t", fmt.Sprintf("%d", opts.Threads))
}
if len(opts.WebSeeds) > 0 {
for _, ws := range opts.WebSeeds {
args = append(args, "-w", ws)
}
}
args = append(args, opts.Target)
return args
}
return renderJSON(w, r, s)
})
var torrentPostHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
file, err := files.NewFileInfo(&files.FileOptions{
@ -128,8 +45,7 @@ var torrentPostHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Req
}
fPath := file.RealPath()
var s *torrent.Torrent
var body torrent.CreateBody
var body users.CreateTorrentBody
if r.Body != nil {
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
return http.StatusBadRequest, fmt.Errorf("failed to decode body: %w", err)
@ -137,36 +53,19 @@ var torrentPostHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Req
defer r.Body.Close()
}
// 设置 mktorrent 命令的选项
options := TorrentOptions{
Target: fPath,
Announces: body.Announces,
Comment: body.Comment,
Date: body.Date,
Name: body.Name,
OutputFile: fPath + ".torrent",
PieceLen: body.PieceLen,
Private: body.Private,
Source: body.Source,
WebSeeds: body.WebSeeds,
}
// 构建 mktorrent 命令的参数列表
args := buildArgs(options)
cmd := exec.Command("mktorrent", args...)
err = cmd.Run()
err = d.Torrent.MakeTorrent(fPath, body)
if err != nil {
return http.StatusInternalServerError, err
}
s = &torrent.Torrent{
Path: fPath + ".torrent",
d.user.CreateBody = body
err = d.store.Users.Update(d.user)
if err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, r, s)
return renderJSON(w, r, nil)
})
var publishPostHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
@ -186,25 +85,13 @@ var publishPostHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Req
// only folder path
fPath := filepath.Dir(tPath)
qbURL := "http://localhost:8081" // 修改为你的qBittorrent URL
username := "moezakura" // 修改为你的用户名
password := "moezakura" // 修改为你的密码
torrentPath := tPath // 修改为你的本地torrent文件路径
savePath := fPath // 修改为你的保存路径
torrentPath := tPath
savePath := fPath
client := &http.Client{}
sid, err := torrent.Login(client, qbURL, username, password)
err = d.Torrent.PublishTorrent(torrentPath, savePath)
if err != nil {
fmt.Printf("Error logging in: %v\n", err)
return http.StatusInternalServerError, err
}
err = torrent.AddTorrent(client, qbURL, sid, torrentPath, savePath)
if err != nil {
fmt.Printf("Error adding torrent: %v\n", err)
}
fmt.Println("Torrent added successfully!")
return renderJSON(w, r, nil)
})

View File

@ -8,16 +8,16 @@ import (
// UserDefaults is a type that holds the default values
// for some fields on User.
type UserDefaults struct {
Scope string `json:"scope"`
Locale string `json:"locale"`
ViewMode users.ViewMode `json:"viewMode"`
SingleClick bool `json:"singleClick"`
Sorting files.Sorting `json:"sorting"`
Perm users.Permissions `json:"perm"`
Commands []string `json:"commands"`
HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"`
TrackerListsUrl string `json:"trackerListsUrl"`
Scope string `json:"scope"`
Locale string `json:"locale"`
ViewMode users.ViewMode `json:"viewMode"`
SingleClick bool `json:"singleClick"`
Sorting files.Sorting `json:"sorting"`
Perm users.Permissions `json:"perm"`
Commands []string `json:"commands"`
HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"`
CreateBody users.CreateTorrentBody `json:"createBody"`
}
// Apply applies the default options to a user.
@ -31,5 +31,5 @@ func (d *UserDefaults) Apply(u *users.User) {
u.Commands = d.Commands
u.HideDotfiles = d.HideDotfiles
u.DateFormat = d.DateFormat
u.TrackerListsUrl = d.TrackerListsUrl
u.CreateBody = d.CreateBody
}

View File

@ -27,6 +27,7 @@ type Settings struct {
Commands map[string][]string `json:"commands"`
Shell []string `json:"shell"`
Rules []rules.Rule `json:"rules"`
Torrent Torrent `json:"torrent"`
}
// GetRules implements rules.Provider.

8
settings/torrent.go Normal file
View File

@ -0,0 +1,8 @@
package settings
type Torrent struct {
TrackersListUrl string `json:"trackersListUrl"`
QbUrl string `json:"qbUrl"`
QbUsername string `json:"qbUsername"`
QbPassword string `json:"qbPassword"`
}

View File

@ -135,6 +135,9 @@ func importConf(db *storm.DB, path string, sto *storage.Storage) error {
Share: true,
Download: true,
},
CreateBody: users.CreateTorrentBody{
Date: true,
},
},
}

97
torrent/mktorrent.go Normal file
View File

@ -0,0 +1,97 @@
package torrent
import (
"fmt"
)
/*
mktorrent 1.1 (c) 2007, 2009 Emil Renner Berthing
Usage: mktorrent [OPTIONS] <target directory or filename>
Options:
-a, --announce=<url>[,<url>]* : specify the full announce URLs
at least one is required
additional -a adds backup trackers
-c, --comment=<comment> : add a comment to the metainfo
-d, --no-date : don't write the creation date
-h, --help : show this help screen
-l, --piece-length=<n> : set the piece length to 2^n bytes,
default is 18, that is 2^18 = 256kb
-n, --name=<name> : set the name of the torrent
default is the basename of the target
-o, --output=<filename> : set the path and filename of the created file
default is <name>.torrent
-p, --private : set the private flag
-s, --source=<source> : add source string embedded in infohash
-t, --threads=<n> : use <n> threads for calculating hashes
default is the number of CPU cores
-v, --verbose : be verbose
-w, --web-seed=<url>[,<url>]* : add web seed URLs
additional -w adds more URLs
*/
// TorrentOptions 结构体定义了 mktorrent 命令的选项
type TorrentOptions struct {
Target string // 目标文件或目录路径
Name string // 种子名称
OutputFile string // 输出种子文件的路径和文件名
Announces []string // 主要的 tracker URLs
Comment string // 添加的注释
Date bool // 是否写入创建日期
PieceLen int // 设置块大小
Private bool // 是否设置为私有种子
Source string // 添加到 infohash 中的源字符串
Threads int // 使用的线程数
WebSeeds []string // Web seed URLs
}
// buildArgs 函数根据 TorrentOptions 构建 mktorrent 命令的参数列表
func buildArgs(opts *TorrentOptions) []string {
args := []string{}
for _, announce := range opts.Announces {
args = append(args, "-a", announce)
}
if opts.Comment != "" {
args = append(args, "-c", opts.Comment)
}
if !opts.Date {
args = append(args, "-d")
}
if opts.PieceLen > 0 {
args = append(args, "-l", fmt.Sprintf("%d", opts.PieceLen))
}
if opts.Name != "" {
args = append(args, "-n", opts.Name)
}
if opts.OutputFile != "" {
args = append(args, "-o", opts.OutputFile)
}
if opts.Private {
args = append(args, "-p")
}
if opts.Source != "" {
args = append(args, "-s", opts.Source)
}
if opts.Threads > 0 {
args = append(args, "-t", fmt.Sprintf("%d", opts.Threads))
}
if len(opts.WebSeeds) > 0 {
for _, ws := range opts.WebSeeds {
args = append(args, "-w", ws)
}
}
args = append(args, opts.Target)
return args
}

View File

@ -10,9 +10,10 @@ import (
"path/filepath"
)
func Login(client *http.Client, qbURL, username, password string) (string, error) {
func login(client *http.Client, qbURL, username, password string) (string, error) {
loginURL := fmt.Sprintf("%s/api/v2/auth/login", qbURL)
data := fmt.Sprintf("username=%s&password=%s", username, password)
req, err := http.NewRequest("POST", loginURL, bytes.NewBufferString(data))
if err != nil {
return "", err
@ -39,7 +40,7 @@ func Login(client *http.Client, qbURL, username, password string) (string, error
return "", fmt.Errorf("SID cookie not found")
}
func AddTorrent(client *http.Client, qbURL, sid, torrentPath, savePath string) error {
func addTorrent(client *http.Client, qbURL, sid, torrentPath, savePath string) error {
addTorrentURL := fmt.Sprintf("%s/api/v2/torrents/add", qbURL)
file, err := os.Open(torrentPath)

View File

@ -1,30 +1,124 @@
package torrent
type CreateBody struct {
Announces []string `json:"announces"`
Comment string `json:"comment"`
Date bool `json:"date"`
Name string `json:"name"`
PieceLen int `json:"pieceLen"`
Private bool `json:"private"`
Source string `json:"source"`
WebSeeds []string `json:"webSeeds"`
}
import (
"fmt"
"io"
"net/http"
"os/exec"
"strings"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
type Torrent struct {
Path string `json:"Path"`
*settings.Settings
*users.User
}
// Link is the information needed to build a shareable link.
// type Torrent struct {
// Hash string `json:"hash" storm:"id,index"`
// Path string `json:"path" storm:"index"`
// UserID uint `json:"userID"`
// Expire int64 `json:"expire"`
// PasswordHash string `json:"password_hash,omitempty"`
// // Token is a random value that will only be set when PasswordHash is set. It is
// // URL-Safe and is used to download links in password-protected shares via a
// // query arg.
// Token string `json:"token,omitempty"`
// }
//
func (t *Torrent) MakeTorrent(fPath string, body users.CreateTorrentBody) error {
tPath := fPath + ".torrent"
// 设置 mktorrent 命令的选项
opts := &TorrentOptions{
Target: fPath,
Announces: body.Announces,
Comment: body.Comment,
Date: body.Date,
Name: body.Name,
OutputFile: tPath,
PieceLen: body.PieceLen,
Private: body.Private,
Source: body.Source,
WebSeeds: body.WebSeeds,
}
args := buildArgs(opts)
cmd := exec.Command("mktorrent", args...)
err := cmd.Run()
if err != nil {
return err
}
return nil
}
func (t *Torrent) PublishTorrent(torrentPath string, savePath string) error {
qbURL := t.Settings.Torrent.QbUrl
username := t.Settings.Torrent.QbUsername
password := t.Settings.Torrent.QbPassword
client := &http.Client{}
sid, err := login(client, qbURL, username, password)
if err != nil {
return fmt.Errorf("failed to login: %v", err)
}
err = addTorrent(client, qbURL, sid, torrentPath, savePath)
if err != nil {
return fmt.Errorf("failed to add torrent: %v", err)
}
return nil
}
func (t *Torrent) GetDefaultCreateBody(createBody users.CreateTorrentBody) (*users.CreateTorrentBody, error) {
announces, err := fetchTrackerList(t.Settings.Torrent.TrackersListUrl)
if err != nil {
return nil, fmt.Errorf("failed to fetch tracker list: %v", err)
}
if createBody.Announces == nil {
createBody.Announces = []string{}
}
if createBody.WebSeeds == nil {
createBody.WebSeeds = []string{}
}
return &users.CreateTorrentBody{
Announces: announces,
Comment: createBody.Comment,
Date: createBody.Date,
Name: "",
PieceLen: createBody.PieceLen,
Private: createBody.Private,
Source: createBody.Source,
WebSeeds: createBody.WebSeeds,
}, nil
}
func fetchTrackerList(url string) ([]string, error) {
// 发送HTTP GET请求
response, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch trackers: %v", err)
}
defer response.Body.Close()
// 检查响应状态
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch trackers: status code %d", response.StatusCode)
}
// 读取响应体
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err)
}
// 移除空行
text := string(body)
text = strings.TrimSpace(text)
lines := strings.Split(text, "\n")
var filteredLines []string
for _, line := range lines {
if strings.TrimSpace(line) != "" {
filteredLines = append(filteredLines, line)
}
}
return filteredLines, nil
}

12
users/torrent.go Normal file
View File

@ -0,0 +1,12 @@
package users
type CreateTorrentBody struct {
Announces []string `json:"announces"`
Comment string `json:"comment"`
Date bool `json:"date"`
Name string `json:"name"`
PieceLen int `json:"pieceLen"`
Private bool `json:"private"`
Source string `json:"source"`
WebSeeds []string `json:"webSeeds"`
}

View File

@ -21,22 +21,22 @@ const (
// User describes a user.
type User struct {
ID uint `storm:"id,increment" json:"id"`
Username string `storm:"unique" json:"username"`
Password string `json:"password"`
Scope string `json:"scope"`
Locale string `json:"locale"`
LockPassword bool `json:"lockPassword"`
ViewMode ViewMode `json:"viewMode"`
SingleClick bool `json:"singleClick"`
Perm Permissions `json:"perm"`
Commands []string `json:"commands"`
Sorting files.Sorting `json:"sorting"`
Fs afero.Fs `json:"-" yaml:"-"`
Rules []rules.Rule `json:"rules"`
HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"`
TrackerListsUrl string `json:"trackerListsUrl"`
ID uint `storm:"id,increment" json:"id"`
Username string `storm:"unique" json:"username"`
Password string `json:"password"`
Scope string `json:"scope"`
Locale string `json:"locale"`
LockPassword bool `json:"lockPassword"`
ViewMode ViewMode `json:"viewMode"`
SingleClick bool `json:"singleClick"`
Perm Permissions `json:"perm"`
Commands []string `json:"commands"`
Sorting files.Sorting `json:"sorting"`
Fs afero.Fs `json:"-" yaml:"-"`
Rules []rules.Rule `json:"rules"`
HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"`
CreateBody CreateTorrentBody `json:"createBody"`
}
// GetRules implements rules.Provider.