lxxweb commited on
Commit
8669cee
·
verified ·
1 Parent(s): e3d48a4

Upload 13 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ client/assets/bitplay_logo.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build stage
2
+ FROM golang:1.24-alpine AS builder
3
+
4
+ # Set working directory for the build
5
+ WORKDIR /app
6
+
7
+ # Copy module files first to leverage Docker cache
8
+ COPY go.mod go.sum ./
9
+ RUN go mod download
10
+
11
+ # Copy all source files including client directory
12
+ COPY . .
13
+
14
+ # Build the Go app with static linking
15
+ RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o main .
16
+
17
+ # Final stage
18
+ FROM alpine:3.18
19
+ RUN apk --no-cache add ca-certificates
20
+
21
+ # Set working directory in final image
22
+ WORKDIR /app
23
+
24
+ # Copy the compiled binary from builder
25
+ COPY --from=builder /app/main .
26
+
27
+ # Copy client directory from builder
28
+ COPY --from=builder /app/client ./client/
29
+
30
+ # Expose the port your app runs on
31
+ EXPOSE 3347
32
+
33
+ # Command to run the application
34
+ CMD ["/app/main"]
client/assets/bitplay_logo.png ADDED

Git LFS Details

  • SHA256: 6e7266be5fb32f36655f1eabb8044b8c81dbb6a677395bef2238e3d6c47b91a1
  • Pointer size: 131 Bytes
  • Size of remote file: 312 kB
client/assets/butterup.min.css ADDED
@@ -0,0 +1 @@
 
 
1
+ .toaster,ol.rack{list-style:none;margin:0}.toaster{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;box-sizing:border-box;padding:5px;outline:0;z-index:999999999;position:fixed}.butteruptoast,.butteruptoast.brutalist{font-size:13px;display:flex;padding:16px;width:325px}.toaster.bottom-right{bottom:20px;right:20px}.toaster.bottom-left{bottom:20px;left:20px}.toaster.top-right{top:20px;right:20px}.toaster.top-left{top:20px;left:20px}.toaster.bottom-center{bottom:20px;left:50%;transform:translateX(-50%)}.toaster.top-center{top:20px;left:50%;transform:translateX(-50%)}.toaster.top-center ol.rack,.toaster.top-left ol.rack,.toaster.top-right ol.rack{flex-direction:column-reverse}.toaster.bottom-center ol.rack,.toaster.bottom-left ol.rack,.toaster.bottom-right ol.rack{flex-direction:column}ol.rack{padding:0;display:flex}ol.rack li{margin-bottom:16px}ol.rack.upperstack li{margin-bottom:-35px;transition:.3s ease-in-out}ol.rack.upperstack li:hover{margin-bottom:16px;scale:1.03;transition:.3s ease-in-out}ol.rack.lowerstack li{margin-top:-35px}ol.rack.lowerstack{margin-bottom:0}.butteruptoast{border-radius:8px;box-shadow:0 4px 12px #0000001a;border:1px solid #ededed;background-color:#fff;gap:6px;color:#282828}.butteruptoast.dismissable{cursor:pointer}.butteruptoast .icon{display:flex;align-items:start;flex-direction:column}.butteruptoast .icon svg{width:20px;height:20px;fill:#282828}.notif .desc{display:flex;flex-direction:column;gap:2px}.notif .desc .title{font-weight:600;line-height:1.5}.notif .desc .message{font-weight:400;line-height:1.4}.butteruptoast.success{background-color:#ebfef2;color:#00892d;border:1px solid #d2fde4}.butteruptoast.success .icon svg{fill:hsl(140,100%,27%)}.butteruptoast.error .icon svg{fill:hsl(0,100%,27%)}.butteruptoast.warning .icon svg{fill:hsl(50,100%,27%)}.butteruptoast.info .icon svg{fill:hsl(210,100%,27%)}.butteruptoast.error{background-color:#fef0f0;color:#890000;border:1px solid #fdd2d2}.butteruptoast.warning{background-color:#fffdf0;color:#897200;border:1px solid #fdf6d2}.butteruptoast.info{background-color:#f0f8ff;color:#004489;border:1px solid #d2e8fd}.toast-buttons{display:flex;gap:8px;width:100%;align-items:center;flex-direction:row;margin-top:16px}.toast-buttons .toast-button.primary{background-color:#282828;color:#fff;padding:8px 16px;border-radius:4px;cursor:pointer;border:none;width:100%}.toast-buttons .toast-button.secondary{background-color:#f0f8ff;color:#004489;border:1px solid #d2e8fd;padding:8px 16px;border-radius:4px;cursor:pointer;width:100%}.butteruptoast.success .toast-button.primary{background-color:#27ae5f;color:#fff}.butteruptoast.success .toast-button.secondary{background-color:#daf0e3;color:#1e8549;border:1px solid #8ae4b0}.butteruptoast.error .toast-button.primary{background-color:#db3748;color:#fff}.butteruptoast.error .toast-button.secondary{background-color:#eddddf;color:#be2131;border:1px solid #eb8e97}.butteruptoast.warning .toast-button.primary{background-color:#ffc005;color:#4c3900}.butteruptoast.warning .toast-button.secondary{background-color:#fff9ea;color:#9e7600;border:1px solid #ffe084}.butteruptoast.info .toast-button.primary{background-color:#2094f3;color:#fff}.butteruptoast.info .toast-button.secondary{background-color:#e1f1fd;color:#085ea4;border:1px solid #81c2f8}.toastUp{animation:.5s ease-in-out forwards slideUp}.toastDown{animation:.5s ease-in-out forwards slideDown}@keyframes slideDown{0%{opacity:0;transform:translateY(-100%)}100%{opacity:1;transform:translateY(0)}}@keyframes slideUp{0%{opacity:0;transform:translateY(100%)}100%{opacity:1;transform:translateY(0)}}.fadeOutToast{animation:.3s ease-in-out forwards fadeOut}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.butteruptoast.glass{background-color:rgba(255,255,255,.42)!important;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:none;box-shadow:0 4px 12px #0000001a;color:#282828}.butteruptoast.glass.success{background-color:rgba(235,254,242,.42)!important;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:none;box-shadow:0 4px 12px #0000001a;color:#00892d}.butteruptoast.glass.error,.butteruptoast.glass.warning{backdrop-filter:blur(10px);border:none;box-shadow:0 4px 12px #0000001a}.butteruptoast.glass.error{background-color:rgba(254,240,240,.42)!important;-webkit-backdrop-filter:blur(10px);color:#890000}.butteruptoast.glass.warning{background-color:rgba(255,253,240,.42)!important;-webkit-backdrop-filter:blur(10px);color:#897200}.butteruptoast.glass.info{background-color:rgba(240,248,255,.42)!important;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:none;box-shadow:0 4px 12px #0000001a;color:#004489}.butteruptoast.brutalist{border-radius:0;box-shadow:0 4px 12px #0000001a;border:2px solid #282828;align-items:center;background-color:#fff;gap:6px;color:#282828}.butteruptoast.brutalist.success{background-color:#ebfef2;color:#00892d;border:2px solid #00892d}.butteruptoast.brutalist.error{background-color:#fef0f0;color:#890000;border:2px solid #890000}.butteruptoast.brutalist.warning{background-color:#fffdf0;color:#897200;border:2px solid #897200}.butteruptoast.brutalist.info{background-color:#f0f8ff;color:#004489;border:2px solid #004489}
client/assets/butterup.min.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ /* butterup version 2.0.0 by Nathan Langer * https://github.com/dgtlss/butterup | https://butterup.nlanger.dev * Thankyou for using butterup! Please consider starring the project on GitHub. */
2
+ var butterup={options:{maxToasts:5,toastLife:5e3,currentToasts:0},toast:function({title:t,message:e,type:n,location:s,icon:o,theme:a,customIcon:l,dismissable:c,onClick:i,onRender:u,onTimeout:d,customHTML:r,primaryButton:m,secondaryButton:p}){if(null==document.getElementById("toaster")){const t=document.createElement("div");if(t.id="toaster",t.className=null==s?"toaster top-right":"toaster "+s,document.body.appendChild(t),null==document.getElementById("butterupRack")){const e=document.createElement("ol");e.id="butterupRack",e.className="rack",t.appendChild(e)}}else{const t=document.getElementById("toaster");t.classList.forEach((function(e){(e.includes("top-right")||e.includes("top-center")||e.includes("top-left")||e.includes("bottom-right")||e.includes("bottom-center")||e.includes("bottom-left"))&&t.classList.remove(e)})),t.className=null==s?"toaster top-right":"toaster "+s,document.getElementById("butterupRack")}if(butterup.options.currentToasts>=butterup.options.maxToasts){var v=document.getElementById("butterupRack").firstChild;document.getElementById("butterupRack").removeChild(v),butterup.options.currentToasts--}const f=document.createElement("li");if(butterup.options.currentToasts++,f.className="butteruptoast",(toaster.className.includes("top-right")||toaster.className.includes("top-center")||toaster.className.includes("top-left"))&&(f.className+=" toastDown"),(toaster.className.includes("bottom-right")||toaster.className.includes("bottom-center")||toaster.className.includes("bottom-left"))&&(f.className+=" toastUp"),f.id="butterupToast-"+butterup.options.currentToasts,null!=n&&(f.className+=" "+n),null!=a&&(f.className+=" "+a),document.getElementById("butterupRack").appendChild(f),null!=o&&1==o){const t=document.createElement("div");t.className="icon",f.appendChild(t),l&&(t.innerHTML=l),null!=n&&null==l&&(f.className+=" "+n,"success"==n&&(t.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /></svg>'),"error"==n&&(t.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" /></svg>'),"warning"==n&&(t.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" /></svg>'),"info"==n&&(t.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" /></svg>'))}const g=document.createElement("div");g.className="notif",f.appendChild(g);const h=document.createElement("div");if(h.className="desc",g.appendChild(h),null!=t){const e=document.createElement("div");e.className="title",e.innerHTML=t,h.appendChild(e)}if(null!=r){const t=document.createElement("div");t.className="message",t.innerHTML=r,h.appendChild(t)}if(null!=e){const t=document.createElement("div");t.className="message",t.innerHTML=e,h.appendChild(t)}if(m||p){const t=document.createElement("div");if(t.className="toast-buttons",g.appendChild(t),m){const e=document.createElement("button");e.className="toast-button primary",e.textContent=m.text,e.onclick=function(t){t.stopPropagation(),m.onClick(t)},t.appendChild(e)}if(p){const e=document.createElement("button");e.className="toast-button secondary",e.textContent=p.text,e.onclick=function(t){t.stopPropagation(),p.onClick(t)},t.appendChild(e)}}i&&"function"==typeof i&&f.addEventListener("click",(function(t){t.stopPropagation(),i(t)})),u&&"function"==typeof u&&u(f),null!=c&&1==c&&(f.className+=" dismissable",f.addEventListener("click",(function(){butterup.despawnToast(f.id)}))),setTimeout((function(){f.className=f.className.replace(" toastDown",""),f.className=f.className.replace(" toastUp","")}),500),setTimeout((function(){d&&"function"==typeof d&&d(f),butterup.despawnToast(f.id)}),butterup.options.toastLife)},despawnToast(t,e){var n=document.getElementById(t);null!=n&&(n.className+=" fadeOutToast",setTimeout((function(){try{n.style.opacity="0",n.parentNode.removeChild(n),butterup.options.currentToasts--,e&&"function"==typeof e&&e(n)}catch(t){}if(0==butterup.options.currentToasts){var t=document.getElementById("toaster");t.parentNode.removeChild(t)}}),500))}};
client/assets/favicon.png ADDED
client/assets/index.css ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ @custom-variant dark (&:is(.dark *));
4
+
5
+ @theme {
6
+ --color-background: var(--background);
7
+ --color-foreground: var(--foreground);
8
+ --color-ring: var(--ring);
9
+ --color-input: var(--input);
10
+ --color-border: var(--border);
11
+ --color-destructive: var(--destructive);
12
+ --color-accent-foreground: var(--accent-foreground);
13
+ --color-accent: var(--accent);
14
+ --color-muted-foreground: var(--muted-foreground);
15
+ --color-muted: var(--muted);
16
+ --color-secondary-foreground: var(--secondary-foreground);
17
+ --color-secondary: var(--secondary);
18
+ --color-primary-foreground: var(--primary-foreground);
19
+ --color-primary: var(--primary);
20
+ --color-popover-foreground: var(--popover-foreground);
21
+ --color-popover: var(--popover);
22
+ --color-card-foreground: var(--card-foreground);
23
+ --color-card: var(--card);
24
+ --radius-sm: calc(var(--radius) - 4px);
25
+ --radius-md: calc(var(--radius) - 2px);
26
+ --radius-lg: var(--radius);
27
+ --radius-xl: calc(var(--radius) + 4px);
28
+ }
29
+
30
+ :root {
31
+ --radius: 0.625rem;
32
+ --background: oklch(1 0 0);
33
+ --foreground: oklch(0.141 0.005 285.823);
34
+ --card: oklch(1 0 0);
35
+ --card-foreground: oklch(0.141 0.005 285.823);
36
+ --popover: oklch(1 0 0);
37
+ --popover-foreground: oklch(0.141 0.005 285.823);
38
+ --primary: oklch(65.24% 0.199188 160.1355);
39
+ --primary-foreground: oklch(0.985 0 0);
40
+ --secondary: oklch(0.967 0.001 286.375);
41
+ --secondary-foreground: oklch(0.21 0.006 285.885);
42
+ --muted: oklch(0.967 0.001 286.375);
43
+ --muted-foreground: oklch(0.552 0.016 285.938);
44
+ --accent: oklch(0.967 0.001 286.375);
45
+ --accent-foreground: oklch(0.21 0.006 285.885);
46
+ --destructive: oklch(0.577 0.245 27.325);
47
+ --border: oklch(0.92 0.004 286.32);
48
+ --input: oklch(0.92 0.004 286.32);
49
+ --ring: oklch(0.705 0.015 286.067);
50
+ }
51
+
52
+ .dark {
53
+ --background: oklch(0.191 0.005 285.823);
54
+ --foreground: oklch(0.985 0 0);
55
+ --card: oklch(0.21 0.006 285.885);
56
+ --card-foreground: oklch(0.985 0 0);
57
+ --popover: oklch(0.21 0.006 285.885);
58
+ --popover-foreground: oklch(0.985 0 0);
59
+ --primary: oklch(88.24% 0.199188 160.1355);
60
+ --primary-foreground: oklch(0.21 0.006 285.885);
61
+ --secondary: oklch(0.244 0.006 286.033);
62
+ --secondary-foreground: oklch(0.985 0 0);
63
+ --muted: oklch(0.274 0.006 286.033);
64
+ --muted-foreground: oklch(0.705 0.015 286.067);
65
+ --accent: oklch(0.274 0.006 286.033);
66
+ --accent-foreground: oklch(0.985 0 0);
67
+ --destructive: oklch(0.704 0.191 22.216);
68
+ --border: oklch(1 0 0 / 10%);
69
+ --input: oklch(1 0 0 / 15%);
70
+ --ring: oklch(0.552 0.016 285.938);
71
+ }
72
+
73
+ @layer base {
74
+ * {
75
+ @apply border-border outline-ring/50;
76
+ }
77
+ body {
78
+ @apply bg-background text-foreground;
79
+ }
80
+ }
81
+
82
+ .btn {
83
+ @apply flex gap-1.5 justify-center items-center min-w-[100px] bg-primary text-primary-foreground h-12 rounded-md px-2 py-2 font-semibold text-sm transition-all hover:bg-primary/90;
84
+ }
85
+
86
+ .btn.small{
87
+ @apply h-8 text-xs min-w-[70px];
88
+ }
89
+
90
+ svg {
91
+ display: inline;
92
+ }
93
+
94
+ #torrent_file_wrapper.drag-over {
95
+ @apply border-primary;
96
+ }
97
+
98
+ #search-pagination {
99
+ @apply flex items-center justify-center gap-2 my-4;
100
+ }
101
+
102
+ #search-pagination.hidden {
103
+ display: none !important;
104
+ }
105
+
106
+ #search-pagination button {
107
+ @apply min-w-[32px] h-[32px] border border-foreground/20 rounded-md p-1 flex items-center justify-center text-xs font-semibold text-sm transition-all hover:bg-accent;
108
+ }
109
+
110
+ #search-pagination button svg {
111
+ font-size: 17px;
112
+ }
113
+
114
+ #search-pagination button.disabled {
115
+ @apply cursor-not-allowed hover:bg-transparent;
116
+ }
117
+
118
+ #search-pagination button.active {
119
+ @apply bg-primary text-primary-foreground;
120
+ }
121
+
122
+ #search-pagination button.active:hover {
123
+ @apply bg-primary text-primary-foreground;
124
+ }
125
+
126
+ #video-player {
127
+ @apply hidden;
128
+ }
129
+
130
+ .btn.loader:before {
131
+ content: "";
132
+ display: block;
133
+ width: 20px;
134
+ height: 20px;
135
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBMaWNlbnNlOiBNSVQuIE1hZGUgYnkgTHVjaWRlIENvbnRyaWJ1dG9yczogaHR0cHM6Ly9sdWNpZGUuZGV2LyAtLT4KPHN2ZyAKICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgd2lkdGg9IjI0IgogIGhlaWdodD0iMjQiCiAgdmlld0JveD0iMCAwIDI0IDI0IgogIGZpbGw9Im5vbmUiCiAgc3Ryb2tlPSIjZmZmZmZmIgogIHN0cm9rZS13aWR0aD0iMiIKICBzdHJva2UtbGluZWNhcD0icm91bmQiCiAgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIKPgogIDxwYXRoIGQ9Ik0yMSAxMmE5IDkgMCAxMS02LjIxOS04LjU2IiAvPgo8L3N2Zz4=);
136
+ background-size: 20px;
137
+ background-repeat: no-repeat;
138
+ background-position: center;
139
+ animation: spin 0.5s linear infinite;
140
+ }
141
+
142
+ .dark .btn.loader:before {
143
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBMaWNlbnNlOiBNSVQuIE1hZGUgYnkgTHVjaWRlIENvbnRyaWJ1dG9yczogaHR0cHM6Ly9sdWNpZGUuZGV2LyAtLT4KPHN2ZyAKICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgd2lkdGg9IjI0IgogIGhlaWdodD0iMjQiCiAgdmlld0JveD0iMCAwIDI0IDI0IgogIGZpbGw9Im5vbmUiCiAgc3Ryb2tlPSIjMDAwMDAwIgogIHN0cm9rZS13aWR0aD0iMiIKICBzdHJva2UtbGluZWNhcD0icm91bmQiCiAgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIKPgogIDxwYXRoIGQ9Ik0yMSAxMmE5IDkgMCAxMS02LjIxOS04LjU2IiAvPgo8L3N2Zz4=);
144
+ }
145
+
146
+ @keyframes spin {
147
+ 0% {
148
+ transform: rotate(0deg);
149
+ }
150
+ 100% {
151
+ transform: rotate(360deg);
152
+ }
153
+ }
154
+
155
+ #toggle_theme {
156
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='1em' height='1em' fill='none' aria-hidden='true' focusable='false'%3E%3Cg%3E%3Cpath d='M21.0672 11.8568L20.4253 11.469L21.0672 11.8568ZM12.1432 2.93276L11.7553 2.29085V2.29085L12.1432 2.93276ZM21.25 12C21.25 17.1086 17.1086 21.25 12 21.25V22.75C17.9371 22.75 22.75 17.9371 22.75 12H21.25ZM12 21.25C6.89137 21.25 2.75 17.1086 2.75 12H1.25C1.25 17.9371 6.06294 22.75 12 22.75V21.25ZM2.75 12C2.75 6.89137 6.89137 2.75 12 2.75V1.25C6.06294 1.25 1.25 6.06294 1.25 12H2.75ZM15.5 14.25C12.3244 14.25 9.75 11.6756 9.75 8.5H8.25C8.25 12.5041 11.4959 15.75 15.5 15.75V14.25ZM20.4253 11.469C19.4172 13.1373 17.5882 14.25 15.5 14.25V15.75C18.1349 15.75 20.4407 14.3439 21.7092 12.2447L20.4253 11.469ZM9.75 8.5C9.75 6.41182 10.8627 4.5828 12.531 3.57467L11.7553 2.29085C9.65609 3.5593 8.25 5.86509 8.25 8.5H9.75ZM12 2.75C11.9115 2.75 11.8077 2.71008 11.7324 2.63168C11.6686 2.56527 11.6538 2.50244 11.6503 2.47703C11.6461 2.44587 11.6482 2.35557 11.7553 2.29085L12.531 3.57467C13.0342 3.27065 13.196 2.71398 13.1368 2.27627C13.0754 1.82126 12.7166 1.25 12 1.25V2.75ZM21.7092 12.2447C21.6444 12.3518 21.5541 12.3539 21.523 12.3497C21.4976 12.3462 21.4347 12.3314 21.3683 12.2676C21.2899 12.1923 21.25 12.0885 21.25 12H22.75C22.75 11.2834 22.1787 10.9246 21.7237 10.8632C21.286 10.804 20.7293 10.9658 20.4253 11.469L21.7092 12.2447Z' fill='%23000' stroke-width='1.5'%3E%3C/path%3E%3C/g%3E%3C/svg%3E")
157
+ center no-repeat;
158
+ }
159
+
160
+ .dark #toggle_theme {
161
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='1em' height='1em' fill='none' aria-hidden='true' focusable='false'%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='5' stroke='%23fff' stroke-width='1.5'%3E%3C/circle%3E%3Cpath d='M12 2V4' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3Cpath d='M12 20V22' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3Cpath d='M4 12L2 12' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3Cpath d='M22 12L20 12' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3Cpath d='M19.7778 4.22266L17.5558 6.25424' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3Cpath d='M4.22217 4.22266L6.44418 6.25424' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3Cpath d='M6.44434 17.5557L4.22211 19.7779' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3Cpath d='M19.7778 19.7773L17.5558 17.5551' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
162
+ }
163
+
164
+ button {
165
+ cursor: pointer;
166
+ transition: all 0.3s ease;
167
+ }
168
+
169
+ button:disabled {
170
+ cursor: not-allowed;
171
+ opacity: 0.6;
172
+ }
173
+
174
+ #video-player {
175
+ min-height: 200px;
176
+ }
177
+
178
+ body .video-js .vjs-big-play-button {
179
+ margin: 0;
180
+ display: flex;
181
+ width: 80px;
182
+ height: 80px;
183
+ transform: translate(-50%, -50%);
184
+ align-items: center;
185
+ justify-content: center;
186
+ border-radius: 50%;
187
+ border-color: var(--color-primary) !important;
188
+ background-color: transparent !important;
189
+ color: var(--color-primary);
190
+ }
191
+
192
+ body .vjs-has-started .vjs-big-play-button {
193
+ display: none;
194
+ }
195
+
196
+ body .video-js .vjs-big-play-button .vjs-icon-placeholder {
197
+ display: flex;
198
+ }
199
+
200
+ body .video-js .vjs-big-play-button .vjs-icon-placeholder:before {
201
+ font-size: 47px;
202
+ position: static;
203
+ }
204
+
205
+ .vjs-progress-control {
206
+ position: absolute !important;
207
+ bottom: 45px !important;
208
+ width: calc(100% - 20px) !important;
209
+ padding: 1px !important;
210
+ height: 40px !important;
211
+ }
212
+
213
+ .video-js .vjs-progress-control .vjs-progress-holder,
214
+ .video-js .vjs-play-progress,
215
+ .video-js .vjs-load-progress div {
216
+ border-radius: 50px;
217
+ }
218
+
219
+ .video-js .vjs-control-bar {
220
+ background: transparent;
221
+ padding: 0 10px 15px;
222
+ height: auto;
223
+ }
224
+
225
+ .video-js .vjs-control-bar:before {
226
+ content: "";
227
+ position: absolute;
228
+ width: 100%;
229
+ height: 200%;
230
+ bottom: 0;
231
+ left: 0;
232
+ background: linear-gradient(
233
+ 0deg,
234
+ rgba(0, 0, 0, 0.84) 0%,
235
+ rgba(0, 0, 0, 0.59) 50%,
236
+ rgba(0, 0, 0, 0) 100%
237
+ );
238
+ }
239
+
240
+ .vjs-remaining-time {
241
+ display: none;
242
+ }
243
+
244
+ .video-js .vjs-current-time,
245
+ .video-js .vjs-duration,
246
+ .vjs-live .vjs-time-control,
247
+ .vjs-time-divider,
248
+ .vjs-live .vjs-time-divider {
249
+ display: block;
250
+ }
251
+
252
+ .video-js .vjs-time-control.vjs-duration {
253
+ margin-right: auto;
254
+ }
255
+
256
+ .video-js .vjs-time-control {
257
+ padding: 0 5px;
258
+ font-size: 15px;
259
+ line-height: 1.2;
260
+ height: 33px;
261
+ display: flex;
262
+ align-items: center;
263
+ }
264
+
265
+ .video-js .vjs-time-control.vjs-time-divider {
266
+ padding: 0;
267
+ min-width: 0;
268
+ font-size: 18px;
269
+ line-height: 1;
270
+ color: white;
271
+ opacity: 1;
272
+ z-index: 1;
273
+ }
274
+
275
+ .video-js .vjs-play-control {
276
+ height: 33px;
277
+ width: 33px;
278
+ display: flex;
279
+ align-items: center;
280
+ justify-content: center;
281
+ }
282
+
283
+ .video-js .vjs-play-control .vjs-icon-placeholder {
284
+ display: flex;
285
+ align-items: center;
286
+ justify-content: center;
287
+ }
288
+
289
+ .video-js .vjs-play-control .vjs-icon-placeholder:before {
290
+ position: relative;
291
+ line-height: 1;
292
+ font-size: 30px;
293
+ }
294
+
295
+ .video-js .vjs-volume-panel .vjs-volume-control {
296
+ opacity: 1;
297
+ width: 60px !important;
298
+ height: 33px;
299
+ }
300
+
301
+ .video-js .vjs-volume-panel {
302
+ width: 110px !important;
303
+ height: 33px;
304
+ margin-left: 6px;
305
+ }
306
+
307
+ .video-js .vjs-mute-control {
308
+ width: 33px;
309
+ height: 33px;
310
+ }
311
+
312
+ .video-js .vjs-mute-control .vjs-icon-placeholder {
313
+ display: flex;
314
+ align-items: center;
315
+ justify-content: center;
316
+ }
317
+
318
+ .video-js .vjs-mute-control .vjs-icon-placeholder:before {
319
+ position: relative;
320
+ line-height: 1;
321
+ font-size: 25px;
322
+ }
323
+
324
+ .video-js .vjs-volume-bar {
325
+ margin: 1.55em 0.45em;
326
+ }
327
+
328
+ .video-js .vjs-time-control.vjs-remaining-time {
329
+ display: none;
330
+ }
331
+
332
+ .video-js .vjs-progress-control .vjs-progress-holder {
333
+ margin: 0 6px;
334
+ }
335
+
336
+ .video-js .vjs-slider {
337
+ background-color: rgb(255 255 255 / 27%);
338
+ }
339
+
340
+ .video-js .vjs-load-progress div {
341
+ background: rgb(255 255 255 / 33%);
342
+ }
343
+
344
+ .video-js .vjs-fullscreen-control,
345
+ .video-js .vjs-picture-in-picture-control,
346
+ .video-js .vjs-subs-caps-button {
347
+ height: 33px;
348
+ width: 33px;
349
+ display: flex;
350
+ align-items: center;
351
+ justify-content: center;
352
+ }
353
+
354
+ .video-js .vjs-fullscreen-control {
355
+ margin-left: 6px;
356
+ }
357
+
358
+ .video-js .vjs-picture-in-picture-control {
359
+ margin-left: 10px;
360
+ }
361
+
362
+ .video-js .vjs-fullscreen-control .vjs-icon-placeholder,
363
+ .video-js .vjs-picture-in-picture-control .vjs-icon-placeholder,
364
+ .video-js .vjs-subs-caps-button .vjs-icon-placeholder {
365
+ display: flex;
366
+ align-items: center;
367
+ justify-content: center;
368
+ }
369
+
370
+ .video-js .vjs-fullscreen-control .vjs-icon-placeholder:before,
371
+ .video-js .vjs-picture-in-picture-control .vjs-icon-placeholder:before,
372
+ .video-js .vjs-subs-caps-button .vjs-icon-placeholder:before {
373
+ position: relative;
374
+ line-height: 1;
375
+ font-size: 28px;
376
+ }
377
+
378
+ .video-js .vjs-picture-in-picture-control .vjs-icon-placeholder:before {
379
+ font-size: 24px;
380
+ }
381
+
382
+ .video-js .vjs-subs-caps-button .vjs-icon-placeholder:before {
383
+ font-size: 26px;
384
+ }
385
+
386
+ .video-select {
387
+ position: absolute;
388
+ top: 15px;
389
+ left: 13px;
390
+ background: transparent;
391
+ display: none;
392
+ font-size: 15px;
393
+ max-width: calc(100% - 25px);
394
+ overflow: hidden;
395
+ text-overflow: ellipsis;
396
+ }
397
+
398
+ .vjs-has-started.vjs-controls-enabled .video-select {
399
+ display: block;
400
+ }
401
+
402
+ .video-js .vjs-progress-holder {
403
+ height: 6px;
404
+ transition: 0.3s all;
405
+ }
406
+
407
+ .video-js .vjs-play-progress:before {
408
+ font-size: 17px;
409
+ transition: 0.3s all;
410
+ }
411
+
412
+ .video-js .vjs-progress-control:hover .vjs-progress-holder {
413
+ height: 8px;
414
+ }
415
+
416
+ .vjs-progress-control:hover .vjs-play-progress:before {
417
+ font-size: 22px;
418
+ }
419
+
420
+ @media (max-width: 768px) {
421
+ .video-js .vjs-control-bar {
422
+ padding: 0 4px 5px;
423
+ }
424
+
425
+ .vjs-progress-control {
426
+ bottom: 31px !important;
427
+ width: calc(100% - 6px) !important;
428
+ padding: 0px !important;
429
+ height: 30px !important;
430
+ }
431
+
432
+ .video-js .vjs-play-control .vjs-icon-placeholder:before {
433
+ font-size: 25px;
434
+ }
435
+
436
+ .video-js .vjs-play-control {
437
+ width: 26px;
438
+ height: 26px;
439
+ }
440
+
441
+ .video-js .vjs-volume-bar {
442
+ margin: 1.15em 0.45em;
443
+ }
444
+
445
+ .video-js .vjs-volume-panel .vjs-volume-control {
446
+ width: 45px !important;
447
+ height: 26px;
448
+ }
449
+
450
+ .video-js .vjs-mute-control {
451
+ width: 26px;
452
+ height: 26px;
453
+ }
454
+
455
+ .video-js .vjs-volume-panel {
456
+ width: 80px !important;
457
+ height: 26px;
458
+ margin-left: 4px;
459
+ }
460
+
461
+ .video-js .vjs-time-control {
462
+ padding: 0 4px;
463
+ font-size: 12px;
464
+ height: 26px;
465
+ }
466
+
467
+ .video-js .vjs-time-control.vjs-time-divider {
468
+ font-size: 15px;
469
+ }
470
+
471
+ .video-js .vjs-fullscreen-control,
472
+ .video-js .vjs-picture-in-picture-control,
473
+ .video-js .vjs-subs-caps-button {
474
+ height: 26px;
475
+ width: 26px;
476
+ }
477
+
478
+ .video-js .vjs-fullscreen-control .vjs-icon-placeholder:before,
479
+ .video-js .vjs-picture-in-picture-control .vjs-icon-placeholder:before,
480
+ .video-js .vjs-subs-caps-button .vjs-icon-placeholder:before {
481
+ font-size: 20px;
482
+ }
483
+
484
+ .video-js .vjs-picture-in-picture-control .vjs-icon-placeholder:before {
485
+ font-size: 18px;
486
+ }
487
+ }
client/assets/index.js ADDED
@@ -0,0 +1,1080 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const getLanguage = (code) => {
2
+ const lang = new Intl.DisplayNames(["en"], { type: "language" });
3
+ return lang.of(code);
4
+ };
5
+
6
+ let settings = {
7
+ enableProxy: false,
8
+ proxyUrl: "",
9
+ enableProwlarr: false,
10
+ prowlarrHost: "",
11
+ prowlarrApiKey: "",
12
+ enableJackett: false,
13
+ jackettHost: "",
14
+ jackettApiKey: "",
15
+ };
16
+
17
+ const searchWrapper = document.querySelector("#search-wrapper");
18
+ var player = null;
19
+
20
+ function doubleTapFF(options) {
21
+ var videoElement = this
22
+ var videoElementId = this.id();
23
+ document.getElementById(videoElementId).addEventListener("touchstart", tapHandler);
24
+ var tapedTwice = false;
25
+ function tapHandler(e) {
26
+ if (!videoElement.paused()) {
27
+
28
+ if (!tapedTwice) {
29
+ tapedTwice = true;
30
+ setTimeout(function () {
31
+ tapedTwice = false;
32
+ }, 300);
33
+ return false;
34
+ }
35
+ e.preventDefault();
36
+ var br = document.getElementById(videoElementId).getBoundingClientRect();
37
+
38
+
39
+ var x = e.touches[0].clientX - br.left;
40
+ var y = e.touches[0].clientY - br.top;
41
+
42
+ if (x <= br.width / 2) {
43
+ videoElement.currentTime(player.currentTime() - 10)
44
+ } else {
45
+ videoElement.currentTime(player.currentTime() + 10)
46
+
47
+ }
48
+ }
49
+
50
+
51
+ }
52
+ }
53
+ videojs.registerPlugin('doubleTapFF', doubleTapFF);
54
+
55
+ (async function ($) {
56
+ // toggle dark mode button
57
+ const toggleDarkMode = () => {
58
+ const html = document.querySelector("html");
59
+ html.classList.toggle("dark");
60
+ localStorage.setItem(
61
+ "theme",
62
+ html.classList.contains("dark") ? "dark" : "light"
63
+ );
64
+ };
65
+ const toggleDarkModeButton = document.querySelector("#toggle_theme");
66
+ toggleDarkModeButton.addEventListener("click", toggleDarkMode);
67
+
68
+ // handle past button
69
+ const pastButton = document.querySelector("#copy_magnet");
70
+ pastButton.addEventListener("click", async () => {
71
+ navigator.clipboard.readText().then((text) => {
72
+ document.getElementById("magnet").value = text;
73
+ });
74
+ });
75
+
76
+ // handle demo button
77
+ const demoButton = document.querySelector("#demo_torrent");
78
+ demoButton.addEventListener("click", async () => {
79
+ document.getElementById("magnet").value =
80
+ "magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent";
81
+
82
+ document
83
+ .querySelector("#torrent-form")
84
+ .dispatchEvent(new Event("submit"));
85
+ });
86
+
87
+ const form = document.querySelector("#torrent-form");
88
+ form.addEventListener("submit", async (e) => {
89
+ e.preventDefault();
90
+ const magnet = document.querySelector("#magnet").value;
91
+
92
+ if (!magnet) {
93
+ butterup.toast({
94
+ message: "Please enter a magnet link",
95
+ location: "top-right",
96
+ icon: true,
97
+ dismissable: true,
98
+ type: "error",
99
+ });
100
+ return;
101
+ }
102
+
103
+ // clean up previous player
104
+ if (player) {
105
+ player.dispose();
106
+ player = null;
107
+ const vidElm = document.createElement("video");
108
+ vidElm.setAttribute("id", "video-player");
109
+ vidElm.setAttribute("class", "video-js mt-10 w-full");
110
+
111
+ document.querySelector("main").appendChild(vidElm);
112
+ }
113
+
114
+ form
115
+ .querySelector("button[type=submit]")
116
+ .setAttribute("disabled", "disabled");
117
+ form.querySelector("button[type=submit]").innerHTML = "";
118
+ form.querySelector("button[type=submit]").classList.add("loader");
119
+
120
+ const res = await fetch("/api/v1/torrent/add", {
121
+ method: "POST",
122
+ headers: { "Content-Type": "application/json" },
123
+ body: JSON.stringify({ magnet }),
124
+ });
125
+
126
+ if (!res.ok) {
127
+ const err = await res.json();
128
+ butterup.toast({
129
+ message: err.error || "Something went wrong",
130
+ location: "top-right",
131
+ icon: true,
132
+ dismissable: true,
133
+ type: "error",
134
+ });
135
+ form.querySelector("button[type=submit]").removeAttribute("disabled");
136
+ form.querySelector("button[type=submit]").innerHTML = "Play Now";
137
+ form.querySelector("button[type=submit]").classList.remove("loader");
138
+ searchResults.querySelectorAll("#play-torrent").forEach((el) => {
139
+ el.removeAttribute("disabled");
140
+ el.innerHTML = "Watch";
141
+ el.classList.remove("loader");
142
+ });
143
+ return;
144
+ }
145
+
146
+ const { sessionId } = await res.json();
147
+ const filesRes = await fetch("/api/v1/torrent/" + sessionId);
148
+
149
+ if (!filesRes.ok) {
150
+ const err = await filesRes.json();
151
+ butterup.toast({
152
+ message: err.error || "Something went wrong",
153
+ location: "top-right",
154
+ icon: true,
155
+ dismissable: true,
156
+ type: "error",
157
+ });
158
+ form.querySelector("button[type=submit]").removeAttribute("disabled");
159
+ form.querySelector("button[type=submit]").innerHTML = "Play Now";
160
+ form.querySelector("button[type=submit]").classList.remove("loader");
161
+ document.querySelectorAll("#play-torrent").forEach((el) => {
162
+ el.removeAttribute("disabled");
163
+ el.innerHTML = "Watch";
164
+ el.classList.remove("loader");
165
+ });
166
+ return;
167
+ }
168
+
169
+ const files = await filesRes.json();
170
+
171
+ // Find video file
172
+ const videoFiles = files.filter((f) =>
173
+ f.name.match(/\.(mp4|mkv|webm|avi)$/i)
174
+ );
175
+
176
+ if (!videoFiles.length) {
177
+ butterup.toast({
178
+ message: "No video file found",
179
+ location: "top-right",
180
+ icon: true,
181
+ dismissable: true,
182
+ type: "error",
183
+ });
184
+ form.querySelector("button[type=submit]").removeAttribute("disabled");
185
+ form.querySelector("button[type=submit]").innerHTML = "Play Now";
186
+ form.querySelector("button[type=submit]").classList.remove("loader");
187
+ document.querySelectorAll("#play-torrent").forEach((el) => {
188
+ el.removeAttribute("disabled");
189
+ el.innerHTML = "Watch";
190
+ el.classList.remove("loader");
191
+ });
192
+ return;
193
+ }
194
+
195
+ const subtitleFiles = files.filter((f) =>
196
+ f.name.match(/\.(srt|vtt|sub)$/i)
197
+ );
198
+
199
+ const videoUrls = videoFiles.map((file) => {
200
+ return {
201
+ src: "/api/v1/torrent/" + sessionId + "/stream/" + file.index,
202
+ title: file.name,
203
+ type: "video/mp4",
204
+ };
205
+ });
206
+
207
+ let subtitles = [];
208
+ if (subtitleFiles.length) {
209
+ subtitles = subtitleFiles.map((subFile) => {
210
+ let language = "en";
211
+ let langName = "English";
212
+
213
+ // Try to extract language code from filename
214
+ console.log(subFile.name);
215
+ const langMatch = subFile.name.match(/\.([a-z]{2,3})\.(srt|vtt|sub)$/i);
216
+ if (langMatch) {
217
+ language = langMatch[1];
218
+ langName = getLanguage(language);
219
+ }
220
+
221
+ return {
222
+ src:
223
+ "/api/v1/torrent/" +
224
+ sessionId +
225
+ "/stream/" +
226
+ subFile.index +
227
+ ".vtt?format=vtt",
228
+ srclang: language,
229
+ label: langName,
230
+ kind: "subtitles",
231
+ type: "vtt",
232
+ };
233
+ });
234
+ }
235
+ player = videojs(
236
+ "video-player",
237
+ {
238
+ fluid: true,
239
+ controls: true,
240
+ autoplay: true,
241
+ preload: "auto",
242
+ sources: [{
243
+ src: videoUrls[0].src,
244
+ type: videoUrls[0].type,
245
+ label: videoUrls[0].title,
246
+ }],
247
+ tracks: subtitles,
248
+ html5: {
249
+ nativeTextTracks: false
250
+ },
251
+ plugins: {
252
+ hotkeys: {
253
+ volumeStep: 0.1,
254
+ seekStep: 5,
255
+ enableModifiersForNumbers: false,
256
+ enableVolumeScroll: false,
257
+ },
258
+ },
259
+ },
260
+ function () {
261
+ player = this;
262
+ player.on("error", (e) => {
263
+ console.error(e);
264
+ butterup.toast({
265
+ message: "Something went wrong",
266
+ location: "top-right",
267
+ icon: true,
268
+ dismissable: true,
269
+ type: "error",
270
+ });
271
+ });
272
+ }
273
+ );
274
+ player.doubleTapFF();
275
+
276
+ document.querySelector("#video-player").style.display = "block";
277
+ // scroll to video player
278
+ setTimeout(() => {
279
+ window.scrollTo({
280
+ top: document.body.scrollHeight,
281
+ behavior: "smooth",
282
+ });
283
+
284
+ if (videoUrls.length > 1) {
285
+ const videoSelect = document.createElement("select");
286
+ videoSelect.setAttribute("id", "video-select");
287
+ videoSelect.setAttribute("class", "video-select");
288
+ videoSelect.setAttribute("aria-label", "Select video");
289
+ videoUrls.forEach((video) => {
290
+ const option = document.createElement("option");
291
+ option.setAttribute("value", video.src);
292
+ option.innerHTML = video.title;
293
+ videoSelect.appendChild(option);
294
+ });
295
+ videoSelect.addEventListener("change", (e) => {
296
+ const selectedSrc = e.target.value;
297
+ player.src({
298
+ src: selectedSrc,
299
+ type: "video/mp4",
300
+ });
301
+ player.play();
302
+ });
303
+ document.querySelector("#video-player").appendChild(videoSelect);
304
+ }
305
+ player.play()
306
+ }, 300);
307
+
308
+ form.querySelector("button[type=submit]").removeAttribute("disabled");
309
+ form.querySelector("button[type=submit]").innerHTML = "Play Now";
310
+ form.querySelector("button[type=submit]").classList.remove("loader");
311
+ document.querySelectorAll("#play-torrent").forEach((el) => {
312
+ el.removeAttribute("disabled");
313
+ el.innerHTML = "Watch";
314
+ el.classList.remove("loader");
315
+ });
316
+ });
317
+
318
+ // create switch button
319
+ const switchInputs = document.querySelectorAll("#switchInput");
320
+ switchInputs.forEach((input) => {
321
+ input.querySelector("input").addEventListener("change", (e) => {
322
+ const dot = e.target.parentElement.querySelector(".dot");
323
+ const wrapper = e.target.parentElement.querySelector(".switch-wrapper");
324
+ if (e.target.checked) {
325
+ dot.classList.add("translate-x-full", "!bg-muted");
326
+ wrapper.classList.add("bg-primary");
327
+ } else {
328
+ dot.classList.remove("translate-x-full", "!bg-muted");
329
+ wrapper.classList.remove("bg-primary");
330
+ }
331
+ });
332
+ });
333
+
334
+ document.querySelector("#settings-btn").addEventListener("click", () => {
335
+ document.querySelector("#settings-model").classList.toggle("hidden");
336
+ });
337
+
338
+ document.querySelectorAll("#close-settings").forEach((el) => {
339
+ el.addEventListener("click", () => {
340
+ document.querySelector("#settings-model").classList.toggle("hidden");
341
+ document.querySelector("#proxy-result").classList.remove("flex");
342
+ document.querySelector("#proxy-result").classList.add("hidden");
343
+ });
344
+ });
345
+
346
+ document.querySelectorAll(".tab-btn").forEach((el) => {
347
+ el.addEventListener("click", () => {
348
+ const tabIndex = el.getAttribute("data-index");
349
+ document.querySelectorAll(".tab").forEach((tab) => {
350
+ const index = tab.getAttribute("data-tab");
351
+ if (index === tabIndex) {
352
+ tab.classList.remove("hidden");
353
+ document.querySelectorAll(".tab-btn").forEach((el) => {
354
+ el.classList.remove("bg-primary", "text-primary-foreground");
355
+ el.classList.add("bg-muted");
356
+ });
357
+ el.classList.add("bg-primary", "text-primary-foreground");
358
+ } else {
359
+ tab.classList.add("hidden");
360
+ }
361
+ });
362
+ });
363
+ });
364
+
365
+ function generatePagination(currentPage, pageSize, total, target) {
366
+ const pagination = document.querySelector(target);
367
+ if (!pagination) return;
368
+ pagination.classList.remove("hidden");
369
+ pagination.innerHTML = "";
370
+ const totalPages = Math.ceil(total / pageSize);
371
+ const startPage = Math.max(1, currentPage - 2);
372
+ const endPage = Math.min(totalPages, currentPage + 2);
373
+
374
+ for (let i = startPage; i <= endPage; i++) {
375
+ const pageButton = document.createElement("button");
376
+ pageButton.textContent = i;
377
+ pageButton.classList.add("page-button");
378
+ if (i === currentPage) {
379
+ pageButton.classList.add("active");
380
+ }
381
+ pageButton.addEventListener("click", () => {
382
+ searchPage = i;
383
+ updateSearchResults();
384
+ });
385
+ pagination.appendChild(pageButton);
386
+ }
387
+ const prevButton = document.createElement("button");
388
+ prevButton.innerHTML = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 20 20" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.72 9.47a.75.75 0 0 0 0 1.06l4.25 4.25a.75.75 0 1 0 1.06-1.06L6.31 10l3.72-3.72a.75.75 0 1 0-1.06-1.06L4.72 9.47Zm9.25-4.25L9.72 9.47a.75.75 0 0 0 0 1.06l4.25 4.25a.75.75 0 1 0 1.06-1.06L11.31 10l3.72-3.72a.75.75 0 0 0-1.06-1.06Z" clip-rule="evenodd"></path></svg>`;
389
+ prevButton.classList.add("page-button");
390
+ prevButton.disabled = currentPage === 1;
391
+ prevButton.addEventListener("click", () => {
392
+ if (currentPage > 1) {
393
+ searchPage--;
394
+ updateSearchResults();
395
+ }
396
+ });
397
+ pagination.prepend(prevButton);
398
+ const nextButton = document.createElement("button");
399
+ nextButton.innerHTML = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 20 20" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.28 9.47a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 1 1-1.06-1.06L13.69 10 9.97 6.28a.75.75 0 0 1 1.06-1.06l4.25 4.25ZM6.03 5.22l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L8.69 10 4.97 6.28a.75.75 0 0 1 1.06-1.06Z" clip-rule="evenodd"></path></svg>`;
400
+ nextButton.classList.add("page-button");
401
+ nextButton.disabled = currentPage === totalPages;
402
+ nextButton.addEventListener("click", () => {
403
+ if (currentPage < totalPages) {
404
+ searchPage++;
405
+ updateSearchResults();
406
+ }
407
+ });
408
+ pagination.appendChild(nextButton);
409
+ }
410
+
411
+ let searchData = [];
412
+ let searchPage = 1;
413
+ let searchPageSize = 5;
414
+
415
+ const updateSearchResults = () => {
416
+ const searchPagination = document.querySelector("#search-pagination");
417
+ const searchResults = document.querySelector("#search-result");
418
+ searchResults.classList.remove("hidden");
419
+ searchResults.querySelector("tbody").innerHTML = "";
420
+ searchResults.querySelector("tfoot").classList.add("hidden");
421
+ if (searchData.length === 0) {
422
+ searchResults.querySelector("tfoot").classList.remove("hidden");
423
+ return;
424
+ }
425
+
426
+ const start = (searchPage - 1) * searchPageSize;
427
+ const end = start + searchPageSize;
428
+ const results = searchData.slice(start, end);
429
+ results.forEach((result) => {
430
+ const resultDiv = document.createElement("tr");
431
+ resultDiv.innerHTML = `
432
+ <td>${result.title}</td>
433
+ <td>${result.indexer}</td>
434
+ <td>${result.size}</td>
435
+ <td>${result.leechers}/${result.seeders}</td>
436
+ <td><button id="play-torrent" type="button" class="btn small" data-magnet="${
437
+ result.downloadUrl || result.magnetUrl
438
+ }">Watch</button></td>
439
+ `;
440
+ searchResults.querySelector("tbody").appendChild(resultDiv);
441
+ });
442
+
443
+ // Generate pagination
444
+ const totalResults = searchData.length;
445
+ const totalPages = Math.ceil(totalResults / searchPageSize);
446
+ generatePagination(
447
+ searchPage,
448
+ searchPageSize,
449
+ totalResults,
450
+ "#search-pagination"
451
+ );
452
+
453
+ // Add event listener to each play button
454
+ searchResults.querySelectorAll("#play-torrent").forEach((el) => {
455
+ el.addEventListener("click", async (e) => {
456
+ const magnet = e.target.getAttribute("data-magnet");
457
+ document.querySelector("#magnet").value = magnet;
458
+ document
459
+ .querySelector("#torrent-form")
460
+ .dispatchEvent(new Event("submit"));
461
+ e.target.setAttribute("disabled", "disabled");
462
+ e.target.innerHTML = "";
463
+ e.target.classList.add("loader");
464
+ });
465
+ });
466
+ };
467
+
468
+ document.querySelector("#search-form").addEventListener("submit", (e) => {
469
+ e.preventDefault();
470
+ const query = e.target.querySelector("#search").value;
471
+ if (!query) {
472
+ butterup.toast({
473
+ message: "Please enter a search query",
474
+ location: "top-right",
475
+ icon: true,
476
+ dismissable: true,
477
+ type: "error",
478
+ });
479
+ return;
480
+ }
481
+
482
+ searchData = [];
483
+ searchPage = 1;
484
+
485
+ e.target
486
+ .querySelector("button[type=submit]")
487
+ .setAttribute("disabled", "disabled");
488
+ e.target.querySelector("button[type=submit]").classList.add("loader");
489
+ e.target.querySelector("button[type=submit]").innerHTML = "";
490
+ const searchResults = document.querySelector("#search-result");
491
+
492
+ searchResults.classList.add("hidden");
493
+ document.querySelector("#search-pagination").classList.add("hidden");
494
+
495
+ let apiUrl = "/api/v1/prowlarr/search";
496
+
497
+ if (
498
+ (!settings.prowlarrHost || !settings.prowlarrApiKey) &&
499
+ settings.jackettHost &&
500
+ settings.jackettApiKey
501
+ ) {
502
+ apiUrl = "/api/v1/jackett/search";
503
+ }
504
+
505
+ fetch(`${apiUrl}?q=${query}`, {
506
+ method: "POST",
507
+ headers: { "Content-Type": "application/json" },
508
+ })
509
+ .then(async (res) => {
510
+ if (!res.ok) {
511
+ const err = await res.json();
512
+ throw new Error(res.error || "Failed to fetch search results");
513
+ }
514
+ return res.json();
515
+ })
516
+ .then((data) => {
517
+ if (data && typeof data === "object") {
518
+ searchData = data;
519
+ } else {
520
+ searchData = [];
521
+ }
522
+
523
+ updateSearchResults();
524
+ })
525
+ .catch((error) => {
526
+ console.error("There was a problem with the fetch operation:", error);
527
+ butterup.toast({
528
+ message: error.message || "Failed to fetch search results",
529
+ location: "top-right",
530
+ icon: true,
531
+ dismissable: true,
532
+ type: "error",
533
+ });
534
+ })
535
+ .finally(() => {
536
+ e.target
537
+ .querySelector("button[type=submit]")
538
+ .removeAttribute("disabled");
539
+ e.target
540
+ .querySelector("button[type=submit]")
541
+ .classList.remove("loader");
542
+ e.target.querySelector("button[type=submit]").innerHTML = "Search";
543
+ });
544
+ });
545
+
546
+ const testProwlarrConfig = async () => {
547
+ const prowlarrHost = document.querySelector("#prowlarrHost").value;
548
+ const prowlarrApiKey = document.querySelector("#prowlarrApiKey").value;
549
+ const prowlarrTestBtn = document.querySelector("#test-prowlarr");
550
+
551
+ if (!prowlarrHost || !prowlarrApiKey) {
552
+ butterup.toast({
553
+ message: "Please enter Prowlarr host and API key",
554
+ location: "top-right",
555
+ icon: true,
556
+ dismissable: true,
557
+ type: "error",
558
+ });
559
+ return false;
560
+ }
561
+
562
+ prowlarrTestBtn.setAttribute("disabled", "disabled");
563
+ prowlarrTestBtn.querySelector("span").innerHTML = "Testing...";
564
+
565
+ const response = await fetch("/api/v1/prowlarr/test", {
566
+ method: "POST",
567
+ headers: { "Content-Type": "application/json" },
568
+ body: JSON.stringify({ prowlarrHost, prowlarrApiKey }),
569
+ });
570
+
571
+ const data = await response.json();
572
+ if (!response.ok) {
573
+ butterup.toast({
574
+ message: data.error || "Failed to test Prowlarr connection",
575
+ location: "top-right",
576
+ icon: true,
577
+ dismissable: true,
578
+ type: "error",
579
+ });
580
+ prowlarrTestBtn.removeAttribute("disabled");
581
+ prowlarrTestBtn.querySelector("span").innerHTML = "Test Connection";
582
+ return false;
583
+ }
584
+
585
+ butterup.toast({
586
+ message: "Prowlarr settings are valid",
587
+ location: "top-right",
588
+ icon: true,
589
+ dismissable: true,
590
+ type: "success",
591
+ });
592
+
593
+ prowlarrTestBtn.removeAttribute("disabled");
594
+ prowlarrTestBtn.querySelector("span").innerHTML = "Test Connection";
595
+
596
+ return true;
597
+ }
598
+
599
+ document.querySelector("#test-prowlarr").addEventListener("click", (e) => {
600
+ testProwlarrConfig();
601
+ });
602
+
603
+ const testJackettConfig = async () => {
604
+ const jackettHost = document.querySelector("#jackettHost").value;
605
+ const jackettApiKey = document.querySelector("#jackettApiKey").value;
606
+ const jackettTestBtn = document.querySelector("#test-jackett");
607
+
608
+ if (!jackettHost || !jackettApiKey) {
609
+ butterup.toast({
610
+ message: "Please enter Jackett host and API key",
611
+ location: "top-right",
612
+ icon: true,
613
+ dismissable: true,
614
+ type: "error",
615
+ });
616
+ return false;
617
+ }
618
+
619
+ jackettTestBtn.setAttribute("disabled", "disabled");
620
+ jackettTestBtn.querySelector("span").innerHTML = "Testing...";
621
+
622
+ const response = await fetch("/api/v1/jackett/test", {
623
+ method: "POST",
624
+ headers: { "Content-Type": "application/json" },
625
+ body: JSON.stringify({ jackettHost, jackettApiKey }),
626
+ });
627
+
628
+ const data = await response.json();
629
+ if (!response.ok) {
630
+ butterup.toast({
631
+ message: data.error || "Failed to test Jackett connection",
632
+ location: "top-right",
633
+ icon: true,
634
+ dismissable: true,
635
+ type: "error",
636
+ });
637
+ jackettTestBtn.removeAttribute("disabled");
638
+ jackettTestBtn.querySelector("span").innerHTML = "Test Connection";
639
+ return false;
640
+ }
641
+
642
+ butterup.toast({
643
+ message: "Jackett settings are valid",
644
+ location: "top-right",
645
+ icon: true,
646
+ dismissable: true,
647
+ type: "success",
648
+ });
649
+
650
+ jackettTestBtn.removeAttribute("disabled");
651
+ jackettTestBtn.querySelector("span").innerHTML = "Test Connection";
652
+
653
+ return true;
654
+ }
655
+
656
+ document.querySelector("#test-jackett").addEventListener("click", (e) => {
657
+ testJackettConfig();
658
+ });
659
+
660
+ const testProxy = async () => {
661
+ const proxyUrl = document.querySelector("#proxyUrl").value;
662
+ const proxyBtn = document.querySelector("#test-proxy");
663
+
664
+ if (!proxyUrl) {
665
+ butterup.toast({
666
+ message: "Please enter a proxy URL",
667
+ location: "top-right",
668
+ icon: true,
669
+ dismissable: true,
670
+ type: "error",
671
+ });
672
+ return false;
673
+ }
674
+
675
+ proxyBtn.setAttribute("disabled", "disabled");
676
+ proxyBtn.querySelector("span").innerHTML = "Testing...";
677
+
678
+ const response = await fetch("/api/v1/proxy/test", {
679
+ method: "POST",
680
+ headers: { "Content-Type": "application/json" },
681
+ body: JSON.stringify({ proxyUrl }),
682
+ });
683
+
684
+ const data = await response.json();
685
+
686
+ if (!response.ok) {
687
+ butterup.toast({
688
+ message: data.error || "Failed to test Proxy connection",
689
+ location: "top-right",
690
+ icon: true,
691
+ dismissable: true,
692
+ type: "error",
693
+ });
694
+ proxyBtn.removeAttribute("disabled");
695
+ proxyBtn.querySelector("span").innerHTML = "Test Proxy";
696
+ return false;
697
+ }
698
+
699
+ butterup.toast({
700
+ message: "Proxy url is valid",
701
+ location: "top-right",
702
+ icon: true,
703
+ dismissable: true,
704
+ type: "success",
705
+ });
706
+
707
+ proxyBtn.removeAttribute("disabled");
708
+ proxyBtn.querySelector("span").innerHTML = "Test Proxy";
709
+
710
+ if (data?.origin) {
711
+ document.querySelector("#proxy-result").classList.remove("hidden");
712
+ document.querySelector("#proxy-result").classList.add("flex");
713
+ document.querySelector("#proxy-result .output-ip").innerHTML = data?.origin
714
+ }
715
+
716
+ return true;
717
+ }
718
+
719
+ document.querySelector("#test-proxy").addEventListener("click", () => {
720
+ testProxy();
721
+ });
722
+
723
+ document
724
+ .querySelector("#proxy-settings-form")
725
+ .addEventListener("submit", async (e) => {
726
+ e.preventDefault();
727
+ const enableProxy = e.target.querySelector("#enableProxy").checked;
728
+ const proxyUrl = e.target.querySelector("#proxyUrl").value;
729
+ const submitButton = e.target.querySelector("button[type=submit]");
730
+
731
+ submitButton.setAttribute("disabled", "disabled");
732
+
733
+ if (enableProxy) {
734
+ const isValid = await testProxy();
735
+ if (!isValid) {
736
+ submitButton.removeAttribute("disabled");
737
+ return;
738
+ }
739
+ }
740
+
741
+ submitButton.classList.add("loader");
742
+ submitButton.innerHTML = "Saving...";
743
+
744
+ const body = {
745
+ enableProxy,
746
+ proxyUrl,
747
+ };
748
+
749
+ const response = await fetch("/api/v1/settings/proxy", {
750
+ method: "POST",
751
+ headers: { "Content-Type": "application/json" },
752
+ body: JSON.stringify(body),
753
+ })
754
+
755
+ const data = await response.json();
756
+
757
+ if (!response.ok) {
758
+ butterup.toast({
759
+ message: data.error || "Failed to save settings",
760
+ location: "top-right",
761
+ icon: true,
762
+ dismissable: true,
763
+ type: "error",
764
+ });
765
+ } else {
766
+ butterup.toast({
767
+ message: "Proxy settings saved successfully",
768
+ location: "top-right",
769
+ icon: true,
770
+ dismissable: true,
771
+ type: "success",
772
+ });
773
+
774
+ settings = {
775
+ ...settings,
776
+ enableProxy: body.enableProxy,
777
+ proxyUrl: body.proxyUrl,
778
+ };
779
+ }
780
+
781
+ submitButton.removeAttribute("disabled");
782
+ submitButton.classList.remove("loader");
783
+ submitButton.innerHTML = "Save Settings";
784
+ });
785
+
786
+ document
787
+ .querySelector("#prowlarr-settings-form")
788
+ .addEventListener("submit", async (e) => {
789
+ e.preventDefault();
790
+ const enableProwlarr = e.target.querySelector("#enableProwlarr").checked;
791
+ const prowlarrHost = e.target.querySelector("#prowlarrHost").value;
792
+ const prowlarrApiKey = e.target.querySelector("#prowlarrApiKey").value;
793
+ const submitButton = e.target.querySelector("button[type=submit]");
794
+
795
+ submitButton.setAttribute("disabled", "disabled");
796
+
797
+ if (enableProwlarr) {
798
+ const isValid = await testProwlarrConfig();
799
+ if (!isValid) {
800
+ submitButton.removeAttribute("disabled");
801
+ return;
802
+ }
803
+ }
804
+
805
+ submitButton.classList.add("loader");
806
+ submitButton.innerHTML = "Saving...";
807
+
808
+ const body = {
809
+ enableProwlarr,
810
+ prowlarrHost,
811
+ prowlarrApiKey,
812
+ };
813
+
814
+ const response = await fetch("/api/v1/settings/prowlarr", {
815
+ method: "POST",
816
+ headers: { "Content-Type": "application/json" },
817
+ body: JSON.stringify(body),
818
+ })
819
+
820
+ const data = await response.json();
821
+ if (!response.ok) {
822
+ butterup.toast({
823
+ message: data.error || "Failed to save settings",
824
+ location: "top-right",
825
+ icon: true,
826
+ dismissable: true,
827
+ type: "error",
828
+ });
829
+ } else {
830
+ butterup.toast({
831
+ message: "Prowlarr settings saved successfully",
832
+ location: "top-right",
833
+ icon: true,
834
+ dismissable: true,
835
+ type: "success",
836
+ });
837
+
838
+ settings = {
839
+ ...settings,
840
+ enableProwlarr: body.enableProwlarr,
841
+ prowlarrHost: body.prowlarrHost,
842
+ prowlarrApiKey: body.prowlarrApiKey,
843
+ };
844
+
845
+ // Check if Prowlarr or Jackett is enabled
846
+ if (body?.enableProwlarr || settings?.enableJackett) {
847
+ searchWrapper.classList.remove("hidden");
848
+ } else {
849
+ searchWrapper.classList.add("hidden");
850
+ }
851
+ }
852
+
853
+ submitButton.removeAttribute("disabled");
854
+ submitButton.classList.remove("loader");
855
+ submitButton.innerHTML = "Save Settings";
856
+ });
857
+
858
+ document
859
+ .querySelector("#jackett-settings-form")
860
+ .addEventListener("submit", async (e) => {
861
+ e.preventDefault();
862
+ const enableJackett = e.target.querySelector("#enableJackett").checked;
863
+ const jackettHost = e.target.querySelector("#jackettHost").value;
864
+ const jackettApiKey = e.target.querySelector("#jackettApiKey").value;
865
+ const submitButton = e.target.querySelector("button[type=submit]");
866
+
867
+ submitButton.setAttribute("disabled", "disabled");
868
+
869
+ if (enableJackett) {
870
+ const isValid = await testJackettConfig();
871
+ if (!isValid) {
872
+ submitButton.removeAttribute("disabled");
873
+ return;
874
+ }
875
+ }
876
+
877
+ submitButton.classList.add("loader");
878
+ submitButton.innerHTML = "Saving...";
879
+
880
+ const body = {
881
+ enableJackett,
882
+ jackettHost,
883
+ jackettApiKey,
884
+ };
885
+
886
+ const response = await fetch("/api/v1/settings/jackett", {
887
+ method: "POST",
888
+ headers: { "Content-Type": "application/json" },
889
+ body: JSON.stringify(body),
890
+ })
891
+
892
+ const data = await response.json();
893
+ if (!response.ok) {
894
+ butterup.toast({
895
+ message: data.error || "Failed to save settings",
896
+ location: "top-right",
897
+ icon: true,
898
+ dismissable: true,
899
+ type: "error",
900
+ });
901
+ } else {
902
+ butterup.toast({
903
+ message: "Jackett settings saved successfully",
904
+ location: "top-right",
905
+ icon: true,
906
+ dismissable: true,
907
+ type: "success",
908
+ });
909
+
910
+ settings = {
911
+ ...settings,
912
+ enableJackett: body.enableJackett,
913
+ jackettHost: body.jackettHost,
914
+ jackettApiKey: body.jackettApiKey,
915
+ };
916
+
917
+ // Check if Jackett or Jackett is enabled
918
+ if (body?.enableJackett || settings?.enableJackett) {
919
+ searchWrapper.classList.remove("hidden");
920
+ } else {
921
+ searchWrapper.classList.add("hidden");
922
+ }
923
+ }
924
+
925
+ submitButton.removeAttribute("disabled");
926
+ submitButton.classList.remove("loader");
927
+ submitButton.innerHTML = "Save Settings";
928
+ });
929
+
930
+ document.querySelector("#torrent_file").addEventListener("change", (e) => {
931
+ const file = e.target.files[0];
932
+ if (file) {
933
+ const formData = new FormData();
934
+ formData.append("torrent", file);
935
+
936
+ fetch("/api/v1/torrent/convert", {
937
+ method: "POST",
938
+ body: formData,
939
+ })
940
+ .then(async (res) => {
941
+ if (!res.ok) {
942
+ const err = await res.json();
943
+ throw new Error(err.error || "Failed to upload torrent file");
944
+ }
945
+ return res.json();
946
+ })
947
+ .then((data) => {
948
+ document.querySelector("#magnet").value = data.magnet;
949
+ document
950
+ .querySelector("#torrent-form")
951
+ .dispatchEvent(new Event("submit"));
952
+ })
953
+ .catch((error) => {
954
+ console.error("There was a problem with the fetch operation:", error);
955
+ butterup.toast({
956
+ message: error.message || "Failed to upload torrent file",
957
+ location: "top-right",
958
+ icon: true,
959
+ dismissable: true,
960
+ type: "error",
961
+ });
962
+ });
963
+ }
964
+ });
965
+
966
+ const torrentFileWrapper = document.querySelector("#torrent_file_wrapper");
967
+ torrentFileWrapper.addEventListener("dragenter", (e) => {
968
+ e.preventDefault();
969
+ e.stopPropagation();
970
+ torrentFileWrapper.classList.add("drag-over");
971
+ });
972
+ torrentFileWrapper.addEventListener("dragover", (e) => {
973
+ e.preventDefault();
974
+ e.stopPropagation();
975
+ });
976
+ torrentFileWrapper.addEventListener("dragleave", (e) => {
977
+ e.preventDefault();
978
+ e.stopPropagation();
979
+ torrentFileWrapper.classList.remove("drag-over");
980
+ });
981
+ torrentFileWrapper.addEventListener("drop", (e) => {
982
+ e.preventDefault();
983
+ e.stopPropagation();
984
+ torrentFileWrapper.classList.remove("drag-over");
985
+ const files = e.dataTransfer.files;
986
+ if (files.length > 0) {
987
+ const file = files[0];
988
+ if (file.name.endsWith(".torrent")) {
989
+ const formData = new FormData();
990
+ formData.append("torrent", file);
991
+
992
+ fetch("/api/v1/torrent/convert", {
993
+ method: "POST",
994
+ body: formData,
995
+ })
996
+ .then(async (res) => {
997
+ if (!res.ok) {
998
+ const err = await res.json();
999
+ throw new Error(err.error || "Failed to upload torrent file");
1000
+ }
1001
+ return res.json();
1002
+ })
1003
+ .then((data) => {
1004
+ document.querySelector("#magnet").value = data.magnet;
1005
+ document
1006
+ .querySelector("#torrent-form")
1007
+ .dispatchEvent(new Event("submit"));
1008
+ })
1009
+ .catch((error) => {
1010
+ console.error(
1011
+ "There was a problem with the fetch operation:",
1012
+ error
1013
+ );
1014
+ butterup.toast({
1015
+ message: error.message || "Failed to upload torrent file",
1016
+ location: "top-right",
1017
+ icon: true,
1018
+ dismissable: true,
1019
+ type: "error",
1020
+ });
1021
+ });
1022
+ } else {
1023
+ butterup.toast({
1024
+ message: "Please drop a valid torrent file",
1025
+ location: "top-right",
1026
+ icon: true,
1027
+ dismissable: true,
1028
+ type: "error",
1029
+ });
1030
+ }
1031
+ }
1032
+ });
1033
+
1034
+ // fetch settings
1035
+ fetch("/api/v1/settings")
1036
+ .then((res) => {
1037
+ if (!res.ok) {
1038
+ throw new Error("Network response was not ok");
1039
+ }
1040
+ return res.json();
1041
+ })
1042
+ .then((data) => {
1043
+ settings = data;
1044
+ document.querySelector("#enableProxy").checked = data.enableProxy;
1045
+ document.querySelector("#proxyUrl").value = data.proxyUrl || "";
1046
+ document.querySelector("#enableProwlarr").checked =
1047
+ data.enableProwlarr || false;
1048
+ document.querySelector("#prowlarrHost").value = data.prowlarrHost || "";
1049
+ document.querySelector("#prowlarrApiKey").value =
1050
+ data.prowlarrApiKey || "";
1051
+ document.querySelector("#enableJackett").checked =
1052
+ data.enableJackett || false;
1053
+ document.querySelector("#jackettHost").value = data.jackettHost || "";
1054
+ document.querySelector("#jackettApiKey").value = data.jackettApiKey || "";
1055
+
1056
+ // Set switch button state
1057
+ const switchInputs = document.querySelectorAll("#switchInput");
1058
+ switchInputs.forEach((input) => {
1059
+ const dot = input.querySelector(".dot");
1060
+ const wrapper = input.querySelector(".switch-wrapper");
1061
+ if (input.querySelector("input").checked) {
1062
+ dot.classList.add("translate-x-full", "!bg-muted");
1063
+ wrapper.classList.add("bg-primary");
1064
+ } else {
1065
+ dot.classList.remove("translate-x-full", "!bg-muted");
1066
+ wrapper.classList.remove("bg-primary");
1067
+ }
1068
+ });
1069
+
1070
+ // Check if Prowlarr or Jackett is enabled
1071
+ if (data?.enableProwlarr || data?.enableJackett) {
1072
+ searchWrapper.classList.remove("hidden");
1073
+ } else {
1074
+ searchWrapper.classList.add("hidden");
1075
+ }
1076
+ })
1077
+ .catch((error) => {
1078
+ console.error("There was a problem with the fetch operation:", error);
1079
+ });
1080
+ })();
client/assets/output.css ADDED
@@ -0,0 +1,1529 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*! tailwindcss v4.1.4 | MIT License | https://tailwindcss.com */
2
+ @layer properties;
3
+ @layer theme, base, components, utilities;
4
+ @layer theme {
5
+ :root, :host {
6
+ --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
7
+ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
8
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
9
+ "Courier New", monospace;
10
+ --color-black: #000;
11
+ --color-white: #fff;
12
+ --spacing: 0.25rem;
13
+ --container-lg: 32rem;
14
+ --container-xl: 36rem;
15
+ --container-4xl: 56rem;
16
+ --text-xs: 0.75rem;
17
+ --text-xs--line-height: calc(1 / 0.75);
18
+ --text-sm: 0.875rem;
19
+ --text-sm--line-height: calc(1.25 / 0.875);
20
+ --text-base: 1rem;
21
+ --text-base--line-height: calc(1.5 / 1);
22
+ --text-xl: 1.25rem;
23
+ --text-xl--line-height: calc(1.75 / 1.25);
24
+ --text-3xl: 1.875rem;
25
+ --text-3xl--line-height: calc(2.25 / 1.875);
26
+ --text-5xl: 3rem;
27
+ --text-5xl--line-height: 1;
28
+ --font-weight-medium: 500;
29
+ --font-weight-semibold: 600;
30
+ --font-weight-bold: 700;
31
+ --radius-md: calc(var(--radius) - 2px);
32
+ --radius-lg: var(--radius);
33
+ --radius-xl: calc(var(--radius) + 4px);
34
+ --default-transition-duration: 150ms;
35
+ --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
36
+ --default-font-family: var(--font-sans);
37
+ --default-mono-font-family: var(--font-mono);
38
+ --color-background: var(--background);
39
+ --color-foreground: var(--foreground);
40
+ --color-ring: var(--ring);
41
+ --color-input: var(--input);
42
+ --color-border: var(--border);
43
+ --color-destructive: var(--destructive);
44
+ --color-accent-foreground: var(--accent-foreground);
45
+ --color-accent: var(--accent);
46
+ --color-muted-foreground: var(--muted-foreground);
47
+ --color-muted: var(--muted);
48
+ --color-secondary: var(--secondary);
49
+ --color-primary-foreground: var(--primary-foreground);
50
+ --color-primary: var(--primary);
51
+ }
52
+ }
53
+ @layer base {
54
+ *, ::after, ::before, ::backdrop, ::file-selector-button {
55
+ box-sizing: border-box;
56
+ margin: 0;
57
+ padding: 0;
58
+ border: 0 solid;
59
+ }
60
+ html, :host {
61
+ line-height: 1.5;
62
+ -webkit-text-size-adjust: 100%;
63
+ tab-size: 4;
64
+ font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
65
+ font-feature-settings: var(--default-font-feature-settings, normal);
66
+ font-variation-settings: var(--default-font-variation-settings, normal);
67
+ -webkit-tap-highlight-color: transparent;
68
+ }
69
+ hr {
70
+ height: 0;
71
+ color: inherit;
72
+ border-top-width: 1px;
73
+ }
74
+ abbr:where([title]) {
75
+ -webkit-text-decoration: underline dotted;
76
+ text-decoration: underline dotted;
77
+ }
78
+ h1, h2, h3, h4, h5, h6 {
79
+ font-size: inherit;
80
+ font-weight: inherit;
81
+ }
82
+ a {
83
+ color: inherit;
84
+ -webkit-text-decoration: inherit;
85
+ text-decoration: inherit;
86
+ }
87
+ b, strong {
88
+ font-weight: bolder;
89
+ }
90
+ code, kbd, samp, pre {
91
+ font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
92
+ font-feature-settings: var(--default-mono-font-feature-settings, normal);
93
+ font-variation-settings: var(--default-mono-font-variation-settings, normal);
94
+ font-size: 1em;
95
+ }
96
+ small {
97
+ font-size: 80%;
98
+ }
99
+ sub, sup {
100
+ font-size: 75%;
101
+ line-height: 0;
102
+ position: relative;
103
+ vertical-align: baseline;
104
+ }
105
+ sub {
106
+ bottom: -0.25em;
107
+ }
108
+ sup {
109
+ top: -0.5em;
110
+ }
111
+ table {
112
+ text-indent: 0;
113
+ border-color: inherit;
114
+ border-collapse: collapse;
115
+ }
116
+ :-moz-focusring {
117
+ outline: auto;
118
+ }
119
+ progress {
120
+ vertical-align: baseline;
121
+ }
122
+ summary {
123
+ display: list-item;
124
+ }
125
+ ol, ul, menu {
126
+ list-style: none;
127
+ }
128
+ img, svg, video, canvas, audio, iframe, embed, object {
129
+ display: block;
130
+ vertical-align: middle;
131
+ }
132
+ img, video {
133
+ max-width: 100%;
134
+ height: auto;
135
+ }
136
+ button, input, select, optgroup, textarea, ::file-selector-button {
137
+ font: inherit;
138
+ font-feature-settings: inherit;
139
+ font-variation-settings: inherit;
140
+ letter-spacing: inherit;
141
+ color: inherit;
142
+ border-radius: 0;
143
+ background-color: transparent;
144
+ opacity: 1;
145
+ }
146
+ :where(select:is([multiple], [size])) optgroup {
147
+ font-weight: bolder;
148
+ }
149
+ :where(select:is([multiple], [size])) optgroup option {
150
+ padding-inline-start: 20px;
151
+ }
152
+ ::file-selector-button {
153
+ margin-inline-end: 4px;
154
+ }
155
+ ::placeholder {
156
+ opacity: 1;
157
+ }
158
+ @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
159
+ ::placeholder {
160
+ color: currentcolor;
161
+ @supports (color: color-mix(in lab, red, red)) {
162
+ color: color-mix(in oklab, currentcolor 50%, transparent);
163
+ }
164
+ }
165
+ }
166
+ textarea {
167
+ resize: vertical;
168
+ }
169
+ ::-webkit-search-decoration {
170
+ -webkit-appearance: none;
171
+ }
172
+ ::-webkit-date-and-time-value {
173
+ min-height: 1lh;
174
+ text-align: inherit;
175
+ }
176
+ ::-webkit-datetime-edit {
177
+ display: inline-flex;
178
+ }
179
+ ::-webkit-datetime-edit-fields-wrapper {
180
+ padding: 0;
181
+ }
182
+ ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
183
+ padding-block: 0;
184
+ }
185
+ :-moz-ui-invalid {
186
+ box-shadow: none;
187
+ }
188
+ button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
189
+ appearance: button;
190
+ }
191
+ ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
192
+ height: auto;
193
+ }
194
+ [hidden]:where(:not([hidden="until-found"])) {
195
+ display: none !important;
196
+ }
197
+ }
198
+ @layer utilities {
199
+ .sr-only {
200
+ position: absolute;
201
+ width: 1px;
202
+ height: 1px;
203
+ padding: 0;
204
+ margin: -1px;
205
+ overflow: hidden;
206
+ clip: rect(0, 0, 0, 0);
207
+ white-space: nowrap;
208
+ border-width: 0;
209
+ }
210
+ .absolute {
211
+ position: absolute;
212
+ }
213
+ .fixed {
214
+ position: fixed;
215
+ }
216
+ .relative {
217
+ position: relative;
218
+ }
219
+ .static {
220
+ position: static;
221
+ }
222
+ .inset-0 {
223
+ inset: calc(var(--spacing) * 0);
224
+ }
225
+ .top-0 {
226
+ top: calc(var(--spacing) * 0);
227
+ }
228
+ .top-\[1px\] {
229
+ top: 1px;
230
+ }
231
+ .right-0 {
232
+ right: calc(var(--spacing) * 0);
233
+ }
234
+ .left-\[1px\] {
235
+ left: 1px;
236
+ }
237
+ .z-\[1\] {
238
+ z-index: 1;
239
+ }
240
+ .z-\[2\] {
241
+ z-index: 2;
242
+ }
243
+ .z-\[999\] {
244
+ z-index: 999;
245
+ }
246
+ .container {
247
+ width: 100%;
248
+ @media (width >= 40rem) {
249
+ max-width: 40rem;
250
+ }
251
+ @media (width >= 48rem) {
252
+ max-width: 48rem;
253
+ }
254
+ @media (width >= 64rem) {
255
+ max-width: 64rem;
256
+ }
257
+ @media (width >= 80rem) {
258
+ max-width: 80rem;
259
+ }
260
+ @media (width >= 96rem) {
261
+ max-width: 96rem;
262
+ }
263
+ }
264
+ .m-auto {
265
+ margin: auto;
266
+ }
267
+ .mx-auto {
268
+ margin-inline: auto;
269
+ }
270
+ .my-4 {
271
+ margin-block: calc(var(--spacing) * 4);
272
+ }
273
+ .mt-4 {
274
+ margin-top: calc(var(--spacing) * 4);
275
+ }
276
+ .mt-7 {
277
+ margin-top: calc(var(--spacing) * 7);
278
+ }
279
+ .mt-10 {
280
+ margin-top: calc(var(--spacing) * 10);
281
+ }
282
+ .mb-6 {
283
+ margin-bottom: calc(var(--spacing) * 6);
284
+ }
285
+ .mb-7 {
286
+ margin-bottom: calc(var(--spacing) * 7);
287
+ }
288
+ .mb-8 {
289
+ margin-bottom: calc(var(--spacing) * 8);
290
+ }
291
+ .mb-12 {
292
+ margin-bottom: calc(var(--spacing) * 12);
293
+ }
294
+ .ml-3 {
295
+ margin-left: calc(var(--spacing) * 3);
296
+ }
297
+ .block {
298
+ display: block;
299
+ }
300
+ .contents {
301
+ display: contents;
302
+ }
303
+ .flex {
304
+ display: flex;
305
+ }
306
+ .hidden {
307
+ display: none;
308
+ }
309
+ .inline-block {
310
+ display: inline-block;
311
+ }
312
+ .inline-flex {
313
+ display: inline-flex;
314
+ }
315
+ .size-5 {
316
+ width: calc(var(--spacing) * 5);
317
+ height: calc(var(--spacing) * 5);
318
+ }
319
+ .size-10 {
320
+ width: calc(var(--spacing) * 10);
321
+ height: calc(var(--spacing) * 10);
322
+ }
323
+ .size-12 {
324
+ width: calc(var(--spacing) * 12);
325
+ height: calc(var(--spacing) * 12);
326
+ }
327
+ .size-full {
328
+ width: 100%;
329
+ height: 100%;
330
+ }
331
+ .\!h-11 {
332
+ height: calc(var(--spacing) * 11) !important;
333
+ }
334
+ .h-4 {
335
+ height: calc(var(--spacing) * 4);
336
+ }
337
+ .h-5 {
338
+ height: calc(var(--spacing) * 5);
339
+ }
340
+ .h-6 {
341
+ height: calc(var(--spacing) * 6);
342
+ }
343
+ .h-10 {
344
+ height: calc(var(--spacing) * 10);
345
+ }
346
+ .h-12 {
347
+ height: calc(var(--spacing) * 12);
348
+ }
349
+ .h-full {
350
+ height: 100%;
351
+ }
352
+ .max-h-\[500px\] {
353
+ max-height: 500px;
354
+ }
355
+ .w-4 {
356
+ width: calc(var(--spacing) * 4);
357
+ }
358
+ .w-5 {
359
+ width: calc(var(--spacing) * 5);
360
+ }
361
+ .w-6 {
362
+ width: calc(var(--spacing) * 6);
363
+ }
364
+ .w-11 {
365
+ width: calc(var(--spacing) * 11);
366
+ }
367
+ .w-12 {
368
+ width: calc(var(--spacing) * 12);
369
+ }
370
+ .w-\[10px\] {
371
+ width: 10px;
372
+ }
373
+ .w-auto {
374
+ width: auto;
375
+ }
376
+ .w-full {
377
+ width: 100%;
378
+ }
379
+ .max-w-4xl {
380
+ max-width: var(--container-4xl);
381
+ }
382
+ .max-w-lg {
383
+ max-width: var(--container-lg);
384
+ }
385
+ .max-w-xl {
386
+ max-width: var(--container-xl);
387
+ }
388
+ .min-w-0 {
389
+ min-width: calc(var(--spacing) * 0);
390
+ }
391
+ .flex-1 {
392
+ flex: 1;
393
+ }
394
+ .shrink-0 {
395
+ flex-shrink: 0;
396
+ }
397
+ .caption-bottom {
398
+ caption-side: bottom;
399
+ }
400
+ .translate-x-full {
401
+ --tw-translate-x: 100%;
402
+ translate: var(--tw-translate-x) var(--tw-translate-y);
403
+ }
404
+ .cursor-pointer {
405
+ cursor: pointer;
406
+ }
407
+ .flex-col {
408
+ flex-direction: column;
409
+ }
410
+ .items-center {
411
+ align-items: center;
412
+ }
413
+ .justify-between {
414
+ justify-content: space-between;
415
+ }
416
+ .justify-center {
417
+ justify-content: center;
418
+ }
419
+ .gap-2 {
420
+ gap: calc(var(--spacing) * 2);
421
+ }
422
+ .gap-3 {
423
+ gap: calc(var(--spacing) * 3);
424
+ }
425
+ .gap-4 {
426
+ gap: calc(var(--spacing) * 4);
427
+ }
428
+ .gap-6 {
429
+ gap: calc(var(--spacing) * 6);
430
+ }
431
+ .divide-y {
432
+ :where(& > :not(:last-child)) {
433
+ --tw-divide-y-reverse: 0;
434
+ border-bottom-style: var(--tw-border-style);
435
+ border-top-style: var(--tw-border-style);
436
+ border-top-width: calc(1px * var(--tw-divide-y-reverse));
437
+ border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
438
+ }
439
+ }
440
+ .overflow-auto {
441
+ overflow: auto;
442
+ }
443
+ .overflow-hidden {
444
+ overflow: hidden;
445
+ }
446
+ .rounded-full {
447
+ border-radius: calc(infinity * 1px);
448
+ }
449
+ .rounded-lg {
450
+ border-radius: var(--radius-lg);
451
+ }
452
+ .rounded-md {
453
+ border-radius: var(--radius-md);
454
+ }
455
+ .rounded-xl {
456
+ border-radius: var(--radius-xl);
457
+ }
458
+ .border {
459
+ border-style: var(--tw-border-style);
460
+ border-width: 1px;
461
+ }
462
+ .border-2 {
463
+ border-style: var(--tw-border-style);
464
+ border-width: 2px;
465
+ }
466
+ .border-b {
467
+ border-bottom-style: var(--tw-border-style);
468
+ border-bottom-width: 1px;
469
+ }
470
+ .border-dashed {
471
+ --tw-border-style: dashed;
472
+ border-style: dashed;
473
+ }
474
+ .border-foreground\/30 {
475
+ border-color: var(--color-foreground);
476
+ @supports (color: color-mix(in lab, red, red)) {
477
+ border-color: color-mix(in oklab, var(--color-foreground) 30%, transparent);
478
+ }
479
+ }
480
+ .border-input {
481
+ border-color: var(--color-input);
482
+ }
483
+ .border-primary {
484
+ border-color: var(--color-primary);
485
+ }
486
+ .\!bg-muted {
487
+ background-color: var(--color-muted) !important;
488
+ }
489
+ .bg-accent {
490
+ background-color: var(--color-accent);
491
+ }
492
+ .bg-accent\/50 {
493
+ background-color: var(--color-accent);
494
+ @supports (color: color-mix(in lab, red, red)) {
495
+ background-color: color-mix(in oklab, var(--color-accent) 50%, transparent);
496
+ }
497
+ }
498
+ .bg-background {
499
+ background-color: var(--color-background);
500
+ }
501
+ .bg-black\/70 {
502
+ background-color: color-mix(in srgb, #000 70%, transparent);
503
+ @supports (color: color-mix(in lab, red, red)) {
504
+ background-color: color-mix(in oklab, var(--color-black) 70%, transparent);
505
+ }
506
+ }
507
+ .bg-muted {
508
+ background-color: var(--color-muted);
509
+ }
510
+ .bg-primary {
511
+ background-color: var(--color-primary);
512
+ }
513
+ .bg-secondary\/50 {
514
+ background-color: var(--color-secondary);
515
+ @supports (color: color-mix(in lab, red, red)) {
516
+ background-color: color-mix(in oklab, var(--color-secondary) 50%, transparent);
517
+ }
518
+ }
519
+ .bg-transparent {
520
+ background-color: transparent;
521
+ }
522
+ .bg-white {
523
+ background-color: var(--color-white);
524
+ }
525
+ .bg-gradient-to-r {
526
+ --tw-gradient-position: to right in oklab;
527
+ background-image: linear-gradient(var(--tw-gradient-stops));
528
+ }
529
+ .from-primary {
530
+ --tw-gradient-from: var(--color-primary);
531
+ --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
532
+ }
533
+ .to-\[\#00CCFF\] {
534
+ --tw-gradient-to: #00CCFF;
535
+ --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
536
+ }
537
+ .bg-clip-text {
538
+ background-clip: text;
539
+ }
540
+ .p-3 {
541
+ padding: calc(var(--spacing) * 3);
542
+ }
543
+ .p-6 {
544
+ padding: calc(var(--spacing) * 6);
545
+ }
546
+ .px-3 {
547
+ padding-inline: calc(var(--spacing) * 3);
548
+ }
549
+ .px-4 {
550
+ padding-inline: calc(var(--spacing) * 4);
551
+ }
552
+ .px-6 {
553
+ padding-inline: calc(var(--spacing) * 6);
554
+ }
555
+ .py-1 {
556
+ padding-block: calc(var(--spacing) * 1);
557
+ }
558
+ .py-2 {
559
+ padding-block: calc(var(--spacing) * 2);
560
+ }
561
+ .py-10 {
562
+ padding-block: calc(var(--spacing) * 10);
563
+ }
564
+ .pr-12 {
565
+ padding-right: calc(var(--spacing) * 12);
566
+ }
567
+ .text-center {
568
+ text-align: center;
569
+ }
570
+ .text-left {
571
+ text-align: left;
572
+ }
573
+ .text-3xl {
574
+ font-size: var(--text-3xl);
575
+ line-height: var(--tw-leading, var(--text-3xl--line-height));
576
+ }
577
+ .text-base {
578
+ font-size: var(--text-base);
579
+ line-height: var(--tw-leading, var(--text-base--line-height));
580
+ }
581
+ .text-sm {
582
+ font-size: var(--text-sm);
583
+ line-height: var(--tw-leading, var(--text-sm--line-height));
584
+ }
585
+ .text-xl {
586
+ font-size: var(--text-xl);
587
+ line-height: var(--tw-leading, var(--text-xl--line-height));
588
+ }
589
+ .text-\[20px\] {
590
+ font-size: 20px;
591
+ }
592
+ .text-\[24px\] {
593
+ font-size: 24px;
594
+ }
595
+ .leading-\[1\.2\] {
596
+ --tw-leading: 1.2;
597
+ line-height: 1.2;
598
+ }
599
+ .font-bold {
600
+ --tw-font-weight: var(--font-weight-bold);
601
+ font-weight: var(--font-weight-bold);
602
+ }
603
+ .font-medium {
604
+ --tw-font-weight: var(--font-weight-medium);
605
+ font-weight: var(--font-weight-medium);
606
+ }
607
+ .font-semibold {
608
+ --tw-font-weight: var(--font-weight-semibold);
609
+ font-weight: var(--font-weight-semibold);
610
+ }
611
+ .overflow-ellipsis {
612
+ text-overflow: ellipsis;
613
+ }
614
+ .whitespace-nowrap {
615
+ white-space: nowrap;
616
+ }
617
+ .text-accent-foreground {
618
+ color: var(--color-accent-foreground);
619
+ }
620
+ .text-muted-foreground {
621
+ color: var(--color-muted-foreground);
622
+ }
623
+ .text-primary {
624
+ color: var(--color-primary);
625
+ }
626
+ .text-primary-foreground {
627
+ color: var(--color-primary-foreground);
628
+ }
629
+ .text-transparent {
630
+ color: transparent;
631
+ }
632
+ .opacity-0 {
633
+ opacity: 0%;
634
+ }
635
+ .shadow {
636
+ --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
637
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
638
+ }
639
+ .shadow-inner {
640
+ --tw-shadow: inset 0 2px 4px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));
641
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
642
+ }
643
+ .shadow-lg {
644
+ --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
645
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
646
+ }
647
+ .shadow-xs {
648
+ --tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05));
649
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
650
+ }
651
+ .outline {
652
+ outline-style: var(--tw-outline-style);
653
+ outline-width: 1px;
654
+ }
655
+ .transition {
656
+ transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter;
657
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
658
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
659
+ }
660
+ .transition-\[color\,box-shadow\] {
661
+ transition-property: color,box-shadow;
662
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
663
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
664
+ }
665
+ .transition-all {
666
+ transition-property: all;
667
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
668
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
669
+ }
670
+ .transition-colors {
671
+ transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
672
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
673
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
674
+ }
675
+ .outline-none {
676
+ --tw-outline-style: none;
677
+ outline-style: none;
678
+ }
679
+ .selection\:bg-primary {
680
+ & *::selection {
681
+ background-color: var(--color-primary);
682
+ }
683
+ &::selection {
684
+ background-color: var(--color-primary);
685
+ }
686
+ }
687
+ .selection\:text-primary-foreground {
688
+ & *::selection {
689
+ color: var(--color-primary-foreground);
690
+ }
691
+ &::selection {
692
+ color: var(--color-primary-foreground);
693
+ }
694
+ }
695
+ .file\:inline-flex {
696
+ &::file-selector-button {
697
+ display: inline-flex;
698
+ }
699
+ }
700
+ .file\:h-7 {
701
+ &::file-selector-button {
702
+ height: calc(var(--spacing) * 7);
703
+ }
704
+ }
705
+ .file\:border-0 {
706
+ &::file-selector-button {
707
+ border-style: var(--tw-border-style);
708
+ border-width: 0px;
709
+ }
710
+ }
711
+ .file\:bg-transparent {
712
+ &::file-selector-button {
713
+ background-color: transparent;
714
+ }
715
+ }
716
+ .file\:text-sm {
717
+ &::file-selector-button {
718
+ font-size: var(--text-sm);
719
+ line-height: var(--tw-leading, var(--text-sm--line-height));
720
+ }
721
+ }
722
+ .file\:font-medium {
723
+ &::file-selector-button {
724
+ --tw-font-weight: var(--font-weight-medium);
725
+ font-weight: var(--font-weight-medium);
726
+ }
727
+ }
728
+ .file\:text-foreground {
729
+ &::file-selector-button {
730
+ color: var(--color-foreground);
731
+ }
732
+ }
733
+ .placeholder\:text-muted-foreground {
734
+ &::placeholder {
735
+ color: var(--color-muted-foreground);
736
+ }
737
+ }
738
+ .hover\:bg-accent {
739
+ &:hover {
740
+ @media (hover: hover) {
741
+ background-color: var(--color-accent);
742
+ }
743
+ }
744
+ }
745
+ .hover\:bg-transparent {
746
+ &:hover {
747
+ @media (hover: hover) {
748
+ background-color: transparent;
749
+ }
750
+ }
751
+ }
752
+ .hover\:text-accent-foreground {
753
+ &:hover {
754
+ @media (hover: hover) {
755
+ color: var(--color-accent-foreground);
756
+ }
757
+ }
758
+ }
759
+ .focus-visible\:border-ring {
760
+ &:focus-visible {
761
+ border-color: var(--color-ring);
762
+ }
763
+ }
764
+ .focus-visible\:ring-\[3px\] {
765
+ &:focus-visible {
766
+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
767
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
768
+ }
769
+ }
770
+ .focus-visible\:ring-ring\/50 {
771
+ &:focus-visible {
772
+ --tw-ring-color: var(--color-ring);
773
+ @supports (color: color-mix(in lab, red, red)) {
774
+ --tw-ring-color: color-mix(in oklab, var(--color-ring) 50%, transparent);
775
+ }
776
+ }
777
+ }
778
+ .disabled\:pointer-events-none {
779
+ &:disabled {
780
+ pointer-events: none;
781
+ }
782
+ }
783
+ .disabled\:cursor-not-allowed {
784
+ &:disabled {
785
+ cursor: not-allowed;
786
+ }
787
+ }
788
+ .disabled\:opacity-50 {
789
+ &:disabled {
790
+ opacity: 50%;
791
+ }
792
+ }
793
+ .aria-invalid\:border-destructive {
794
+ &[aria-invalid="true"] {
795
+ border-color: var(--color-destructive);
796
+ }
797
+ }
798
+ .aria-invalid\:ring-destructive\/20 {
799
+ &[aria-invalid="true"] {
800
+ --tw-ring-color: var(--color-destructive);
801
+ @supports (color: color-mix(in lab, red, red)) {
802
+ --tw-ring-color: color-mix(in oklab, var(--color-destructive) 20%, transparent);
803
+ }
804
+ }
805
+ }
806
+ .sm\:px-6 {
807
+ @media (width >= 40rem) {
808
+ padding-inline: calc(var(--spacing) * 6);
809
+ }
810
+ }
811
+ .md\:mb-15 {
812
+ @media (width >= 48rem) {
813
+ margin-bottom: calc(var(--spacing) * 15);
814
+ }
815
+ }
816
+ .md\:text-5xl {
817
+ @media (width >= 48rem) {
818
+ font-size: var(--text-5xl);
819
+ line-height: var(--tw-leading, var(--text-5xl--line-height));
820
+ }
821
+ }
822
+ .md\:text-sm {
823
+ @media (width >= 48rem) {
824
+ font-size: var(--text-sm);
825
+ line-height: var(--tw-leading, var(--text-sm--line-height));
826
+ }
827
+ }
828
+ .lg\:px-8 {
829
+ @media (width >= 64rem) {
830
+ padding-inline: calc(var(--spacing) * 8);
831
+ }
832
+ }
833
+ .dark\:border-input {
834
+ &:is(.dark *) {
835
+ border-color: var(--color-input);
836
+ }
837
+ }
838
+ .dark\:bg-input\/30 {
839
+ &:is(.dark *) {
840
+ background-color: var(--color-input);
841
+ @supports (color: color-mix(in lab, red, red)) {
842
+ background-color: color-mix(in oklab, var(--color-input) 30%, transparent);
843
+ }
844
+ }
845
+ }
846
+ .dark\:hover\:bg-accent\/50 {
847
+ &:is(.dark *) {
848
+ &:hover {
849
+ @media (hover: hover) {
850
+ background-color: var(--color-accent);
851
+ @supports (color: color-mix(in lab, red, red)) {
852
+ background-color: color-mix(in oklab, var(--color-accent) 50%, transparent);
853
+ }
854
+ }
855
+ }
856
+ }
857
+ }
858
+ .dark\:hover\:bg-input\/50 {
859
+ &:is(.dark *) {
860
+ &:hover {
861
+ @media (hover: hover) {
862
+ background-color: var(--color-input);
863
+ @supports (color: color-mix(in lab, red, red)) {
864
+ background-color: color-mix(in oklab, var(--color-input) 50%, transparent);
865
+ }
866
+ }
867
+ }
868
+ }
869
+ }
870
+ .dark\:aria-invalid\:ring-destructive\/40 {
871
+ &:is(.dark *) {
872
+ &[aria-invalid="true"] {
873
+ --tw-ring-color: var(--color-destructive);
874
+ @supports (color: color-mix(in lab, red, red)) {
875
+ --tw-ring-color: color-mix(in oklab, var(--color-destructive) 40%, transparent);
876
+ }
877
+ }
878
+ }
879
+ }
880
+ .\[\&_td\]\:max-w-\[200px\] {
881
+ & td {
882
+ max-width: 200px;
883
+ }
884
+ }
885
+ .\[\&_td\]\:overflow-hidden {
886
+ & td {
887
+ overflow: hidden;
888
+ }
889
+ }
890
+ .\[\&_td\]\:px-3 {
891
+ & td {
892
+ padding-inline: calc(var(--spacing) * 3);
893
+ }
894
+ }
895
+ .\[\&_td\]\:py-2 {
896
+ & td {
897
+ padding-block: calc(var(--spacing) * 2);
898
+ }
899
+ }
900
+ .\[\&_td\]\:text-ellipsis {
901
+ & td {
902
+ text-overflow: ellipsis;
903
+ }
904
+ }
905
+ .\[\&_td\]\:whitespace-nowrap {
906
+ & td {
907
+ white-space: nowrap;
908
+ }
909
+ }
910
+ }
911
+ :root {
912
+ --radius: 0.625rem;
913
+ --background: oklch(1 0 0);
914
+ --foreground: oklch(0.141 0.005 285.823);
915
+ --card: oklch(1 0 0);
916
+ --card-foreground: oklch(0.141 0.005 285.823);
917
+ --popover: oklch(1 0 0);
918
+ --popover-foreground: oklch(0.141 0.005 285.823);
919
+ --primary: oklch(65.24% 0.199188 160.1355);
920
+ --primary-foreground: oklch(0.985 0 0);
921
+ --secondary: oklch(0.967 0.001 286.375);
922
+ --secondary-foreground: oklch(0.21 0.006 285.885);
923
+ --muted: oklch(0.967 0.001 286.375);
924
+ --muted-foreground: oklch(0.552 0.016 285.938);
925
+ --accent: oklch(0.967 0.001 286.375);
926
+ --accent-foreground: oklch(0.21 0.006 285.885);
927
+ --destructive: oklch(0.577 0.245 27.325);
928
+ --border: oklch(0.92 0.004 286.32);
929
+ --input: oklch(0.92 0.004 286.32);
930
+ --ring: oklch(0.705 0.015 286.067);
931
+ }
932
+ .dark {
933
+ --background: oklch(0.191 0.005 285.823);
934
+ --foreground: oklch(0.985 0 0);
935
+ --card: oklch(0.21 0.006 285.885);
936
+ --card-foreground: oklch(0.985 0 0);
937
+ --popover: oklch(0.21 0.006 285.885);
938
+ --popover-foreground: oklch(0.985 0 0);
939
+ --primary: oklch(88.24% 0.199188 160.1355);
940
+ --primary-foreground: oklch(0.21 0.006 285.885);
941
+ --secondary: oklch(0.244 0.006 286.033);
942
+ --secondary-foreground: oklch(0.985 0 0);
943
+ --muted: oklch(0.274 0.006 286.033);
944
+ --muted-foreground: oklch(0.705 0.015 286.067);
945
+ --accent: oklch(0.274 0.006 286.033);
946
+ --accent-foreground: oklch(0.985 0 0);
947
+ --destructive: oklch(0.704 0.191 22.216);
948
+ --border: oklch(1 0 0 / 10%);
949
+ --input: oklch(1 0 0 / 15%);
950
+ --ring: oklch(0.552 0.016 285.938);
951
+ }
952
+ @layer base {
953
+ * {
954
+ border-color: var(--color-border);
955
+ outline-color: var(--color-ring);
956
+ @supports (color: color-mix(in lab, red, red)) {
957
+ outline-color: color-mix(in oklab, var(--color-ring) 50%, transparent);
958
+ }
959
+ }
960
+ body {
961
+ background-color: var(--color-background);
962
+ color: var(--color-foreground);
963
+ }
964
+ }
965
+ .btn {
966
+ display: flex;
967
+ height: calc(var(--spacing) * 12);
968
+ min-width: 100px;
969
+ align-items: center;
970
+ justify-content: center;
971
+ gap: calc(var(--spacing) * 1.5);
972
+ border-radius: var(--radius-md);
973
+ background-color: var(--color-primary);
974
+ padding-inline: calc(var(--spacing) * 2);
975
+ padding-block: calc(var(--spacing) * 2);
976
+ font-size: var(--text-sm);
977
+ line-height: var(--tw-leading, var(--text-sm--line-height));
978
+ --tw-font-weight: var(--font-weight-semibold);
979
+ font-weight: var(--font-weight-semibold);
980
+ color: var(--color-primary-foreground);
981
+ transition-property: all;
982
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
983
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
984
+ &:hover {
985
+ @media (hover: hover) {
986
+ background-color: var(--color-primary);
987
+ @supports (color: color-mix(in lab, red, red)) {
988
+ background-color: color-mix(in oklab, var(--color-primary) 90%, transparent);
989
+ }
990
+ }
991
+ }
992
+ }
993
+ .btn.small {
994
+ height: calc(var(--spacing) * 8);
995
+ min-width: 70px;
996
+ font-size: var(--text-xs);
997
+ line-height: var(--tw-leading, var(--text-xs--line-height));
998
+ }
999
+ svg {
1000
+ display: inline;
1001
+ }
1002
+ #torrent_file_wrapper.drag-over {
1003
+ border-color: var(--color-primary);
1004
+ }
1005
+ #search-pagination {
1006
+ margin-block: calc(var(--spacing) * 4);
1007
+ display: flex;
1008
+ align-items: center;
1009
+ justify-content: center;
1010
+ gap: calc(var(--spacing) * 2);
1011
+ }
1012
+ #search-pagination.hidden {
1013
+ display: none !important;
1014
+ }
1015
+ #search-pagination button {
1016
+ display: flex;
1017
+ height: 32px;
1018
+ min-width: 32px;
1019
+ align-items: center;
1020
+ justify-content: center;
1021
+ border-radius: var(--radius-md);
1022
+ border-style: var(--tw-border-style);
1023
+ border-width: 1px;
1024
+ border-color: var(--color-foreground);
1025
+ @supports (color: color-mix(in lab, red, red)) {
1026
+ border-color: color-mix(in oklab, var(--color-foreground) 20%, transparent);
1027
+ }
1028
+ padding: calc(var(--spacing) * 1);
1029
+ font-size: var(--text-sm);
1030
+ line-height: var(--tw-leading, var(--text-sm--line-height));
1031
+ font-size: var(--text-xs);
1032
+ line-height: var(--tw-leading, var(--text-xs--line-height));
1033
+ --tw-font-weight: var(--font-weight-semibold);
1034
+ font-weight: var(--font-weight-semibold);
1035
+ transition-property: all;
1036
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
1037
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
1038
+ &:hover {
1039
+ @media (hover: hover) {
1040
+ background-color: var(--color-accent);
1041
+ }
1042
+ }
1043
+ }
1044
+ #search-pagination button svg {
1045
+ font-size: 17px;
1046
+ }
1047
+ #search-pagination button.disabled {
1048
+ cursor: not-allowed;
1049
+ &:hover {
1050
+ @media (hover: hover) {
1051
+ background-color: transparent;
1052
+ }
1053
+ }
1054
+ }
1055
+ #search-pagination button.active {
1056
+ background-color: var(--color-primary);
1057
+ color: var(--color-primary-foreground);
1058
+ }
1059
+ #search-pagination button.active:hover {
1060
+ background-color: var(--color-primary);
1061
+ color: var(--color-primary-foreground);
1062
+ }
1063
+ #video-player {
1064
+ display: none;
1065
+ }
1066
+ .btn.loader:before {
1067
+ content: "";
1068
+ display: block;
1069
+ width: 20px;
1070
+ height: 20px;
1071
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBMaWNlbnNlOiBNSVQuIE1hZGUgYnkgTHVjaWRlIENvbnRyaWJ1dG9yczogaHR0cHM6Ly9sdWNpZGUuZGV2LyAtLT4KPHN2ZyAKICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgd2lkdGg9IjI0IgogIGhlaWdodD0iMjQiCiAgdmlld0JveD0iMCAwIDI0IDI0IgogIGZpbGw9Im5vbmUiCiAgc3Ryb2tlPSIjZmZmZmZmIgogIHN0cm9rZS13aWR0aD0iMiIKICBzdHJva2UtbGluZWNhcD0icm91bmQiCiAgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIKPgogIDxwYXRoIGQ9Ik0yMSAxMmE5IDkgMCAxMS02LjIxOS04LjU2IiAvPgo8L3N2Zz4=);
1072
+ background-size: 20px;
1073
+ background-repeat: no-repeat;
1074
+ background-position: center;
1075
+ animation: spin 0.5s linear infinite;
1076
+ }
1077
+ .dark .btn.loader:before {
1078
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBMaWNlbnNlOiBNSVQuIE1hZGUgYnkgTHVjaWRlIENvbnRyaWJ1dG9yczogaHR0cHM6Ly9sdWNpZGUuZGV2LyAtLT4KPHN2ZyAKICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgd2lkdGg9IjI0IgogIGhlaWdodD0iMjQiCiAgdmlld0JveD0iMCAwIDI0IDI0IgogIGZpbGw9Im5vbmUiCiAgc3Ryb2tlPSIjMDAwMDAwIgogIHN0cm9rZS13aWR0aD0iMiIKICBzdHJva2UtbGluZWNhcD0icm91bmQiCiAgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIKPgogIDxwYXRoIGQ9Ik0yMSAxMmE5IDkgMCAxMS02LjIxOS04LjU2IiAvPgo8L3N2Zz4=);
1079
+ }
1080
+ @keyframes spin {
1081
+ 0% {
1082
+ transform: rotate(0deg);
1083
+ }
1084
+ 100% {
1085
+ transform: rotate(360deg);
1086
+ }
1087
+ }
1088
+ #toggle_theme {
1089
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='1em' height='1em' fill='none' aria-hidden='true' focusable='false'%3E%3Cg%3E%3Cpath d='M21.0672 11.8568L20.4253 11.469L21.0672 11.8568ZM12.1432 2.93276L11.7553 2.29085V2.29085L12.1432 2.93276ZM21.25 12C21.25 17.1086 17.1086 21.25 12 21.25V22.75C17.9371 22.75 22.75 17.9371 22.75 12H21.25ZM12 21.25C6.89137 21.25 2.75 17.1086 2.75 12H1.25C1.25 17.9371 6.06294 22.75 12 22.75V21.25ZM2.75 12C2.75 6.89137 6.89137 2.75 12 2.75V1.25C6.06294 1.25 1.25 6.06294 1.25 12H2.75ZM15.5 14.25C12.3244 14.25 9.75 11.6756 9.75 8.5H8.25C8.25 12.5041 11.4959 15.75 15.5 15.75V14.25ZM20.4253 11.469C19.4172 13.1373 17.5882 14.25 15.5 14.25V15.75C18.1349 15.75 20.4407 14.3439 21.7092 12.2447L20.4253 11.469ZM9.75 8.5C9.75 6.41182 10.8627 4.5828 12.531 3.57467L11.7553 2.29085C9.65609 3.5593 8.25 5.86509 8.25 8.5H9.75ZM12 2.75C11.9115 2.75 11.8077 2.71008 11.7324 2.63168C11.6686 2.56527 11.6538 2.50244 11.6503 2.47703C11.6461 2.44587 11.6482 2.35557 11.7553 2.29085L12.531 3.57467C13.0342 3.27065 13.196 2.71398 13.1368 2.27627C13.0754 1.82126 12.7166 1.25 12 1.25V2.75ZM21.7092 12.2447C21.6444 12.3518 21.5541 12.3539 21.523 12.3497C21.4976 12.3462 21.4347 12.3314 21.3683 12.2676C21.2899 12.1923 21.25 12.0885 21.25 12H22.75C22.75 11.2834 22.1787 10.9246 21.7237 10.8632C21.286 10.804 20.7293 10.9658 20.4253 11.469L21.7092 12.2447Z' fill='%23000' stroke-width='1.5'%3E%3C/path%3E%3C/g%3E%3C/svg%3E") center no-repeat;
1090
+ }
1091
+ .dark #toggle_theme {
1092
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='1em' height='1em' fill='none' aria-hidden='true' focusable='false'%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='5' stroke='%23fff' stroke-width='1.5'%3E%3C/circle%3E%3Cpath d='M12 2V4' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3Cpath d='M12 20V22' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3Cpath d='M4 12L2 12' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3Cpath d='M22 12L20 12' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3Cpath d='M19.7778 4.22266L17.5558 6.25424' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3Cpath d='M4.22217 4.22266L6.44418 6.25424' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3Cpath d='M6.44434 17.5557L4.22211 19.7779' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3Cpath d='M19.7778 19.7773L17.5558 17.5551' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
1093
+ }
1094
+ button {
1095
+ cursor: pointer;
1096
+ transition: all 0.3s ease;
1097
+ }
1098
+ button:disabled {
1099
+ cursor: not-allowed;
1100
+ opacity: 0.6;
1101
+ }
1102
+ #video-player {
1103
+ min-height: 200px;
1104
+ }
1105
+ body .video-js .vjs-big-play-button {
1106
+ margin: 0;
1107
+ display: flex;
1108
+ width: 80px;
1109
+ height: 80px;
1110
+ transform: translate(-50%, -50%);
1111
+ align-items: center;
1112
+ justify-content: center;
1113
+ border-radius: 50%;
1114
+ border-color: var(--color-primary) !important;
1115
+ background-color: transparent !important;
1116
+ color: var(--color-primary);
1117
+ }
1118
+ body .vjs-has-started .vjs-big-play-button {
1119
+ display: none;
1120
+ }
1121
+ body .video-js .vjs-big-play-button .vjs-icon-placeholder {
1122
+ display: flex;
1123
+ }
1124
+ body .video-js .vjs-big-play-button .vjs-icon-placeholder:before {
1125
+ font-size: 47px;
1126
+ position: static;
1127
+ }
1128
+ .vjs-progress-control {
1129
+ position: absolute !important;
1130
+ bottom: 45px !important;
1131
+ width: calc(100% - 20px) !important;
1132
+ padding: 1px !important;
1133
+ height: 40px !important;
1134
+ }
1135
+ .video-js .vjs-progress-control .vjs-progress-holder, .video-js .vjs-play-progress, .video-js .vjs-load-progress div {
1136
+ border-radius: 50px;
1137
+ }
1138
+ .video-js .vjs-control-bar {
1139
+ background: transparent;
1140
+ padding: 0 10px 15px;
1141
+ height: auto;
1142
+ }
1143
+ .video-js .vjs-control-bar:before {
1144
+ content: "";
1145
+ position: absolute;
1146
+ width: 100%;
1147
+ height: 200%;
1148
+ bottom: 0;
1149
+ left: 0;
1150
+ background: linear-gradient( 0deg, rgba(0, 0, 0, 0.84) 0%, rgba(0, 0, 0, 0.59) 50%, rgba(0, 0, 0, 0) 100% );
1151
+ }
1152
+ .vjs-remaining-time {
1153
+ display: none;
1154
+ }
1155
+ .video-js .vjs-current-time, .video-js .vjs-duration, .vjs-live .vjs-time-control, .vjs-time-divider, .vjs-live .vjs-time-divider {
1156
+ display: block;
1157
+ }
1158
+ .video-js .vjs-time-control.vjs-duration {
1159
+ margin-right: auto;
1160
+ }
1161
+ .video-js .vjs-time-control {
1162
+ padding: 0 5px;
1163
+ font-size: 15px;
1164
+ line-height: 1.2;
1165
+ height: 33px;
1166
+ display: flex;
1167
+ align-items: center;
1168
+ }
1169
+ .video-js .vjs-time-control.vjs-time-divider {
1170
+ padding: 0;
1171
+ min-width: 0;
1172
+ font-size: 18px;
1173
+ line-height: 1;
1174
+ color: white;
1175
+ opacity: 1;
1176
+ z-index: 1;
1177
+ }
1178
+ .video-js .vjs-play-control {
1179
+ height: 33px;
1180
+ width: 33px;
1181
+ display: flex;
1182
+ align-items: center;
1183
+ justify-content: center;
1184
+ }
1185
+ .video-js .vjs-play-control .vjs-icon-placeholder {
1186
+ display: flex;
1187
+ align-items: center;
1188
+ justify-content: center;
1189
+ }
1190
+ .video-js .vjs-play-control .vjs-icon-placeholder:before {
1191
+ position: relative;
1192
+ line-height: 1;
1193
+ font-size: 30px;
1194
+ }
1195
+ .video-js .vjs-volume-panel .vjs-volume-control {
1196
+ opacity: 1;
1197
+ width: 60px !important;
1198
+ height: 33px;
1199
+ }
1200
+ .video-js .vjs-volume-panel {
1201
+ width: 110px !important;
1202
+ height: 33px;
1203
+ margin-left: 6px;
1204
+ }
1205
+ .video-js .vjs-mute-control {
1206
+ width: 33px;
1207
+ height: 33px;
1208
+ }
1209
+ .video-js .vjs-mute-control .vjs-icon-placeholder {
1210
+ display: flex;
1211
+ align-items: center;
1212
+ justify-content: center;
1213
+ }
1214
+ .video-js .vjs-mute-control .vjs-icon-placeholder:before {
1215
+ position: relative;
1216
+ line-height: 1;
1217
+ font-size: 25px;
1218
+ }
1219
+ .video-js .vjs-volume-bar {
1220
+ margin: 1.55em 0.45em;
1221
+ }
1222
+ .video-js .vjs-time-control.vjs-remaining-time {
1223
+ display: none;
1224
+ }
1225
+ .video-js .vjs-progress-control .vjs-progress-holder {
1226
+ margin: 0 6px;
1227
+ }
1228
+ .video-js .vjs-slider {
1229
+ background-color: rgb(255 255 255 / 27%);
1230
+ }
1231
+ .video-js .vjs-load-progress div {
1232
+ background: rgb(255 255 255 / 33%);
1233
+ }
1234
+ .video-js .vjs-fullscreen-control, .video-js .vjs-picture-in-picture-control, .video-js .vjs-subs-caps-button {
1235
+ height: 33px;
1236
+ width: 33px;
1237
+ display: flex;
1238
+ align-items: center;
1239
+ justify-content: center;
1240
+ }
1241
+ .video-js .vjs-fullscreen-control {
1242
+ margin-left: 6px;
1243
+ }
1244
+ .video-js .vjs-picture-in-picture-control {
1245
+ margin-left: 10px;
1246
+ }
1247
+ .video-js .vjs-fullscreen-control .vjs-icon-placeholder, .video-js .vjs-picture-in-picture-control .vjs-icon-placeholder, .video-js .vjs-subs-caps-button .vjs-icon-placeholder {
1248
+ display: flex;
1249
+ align-items: center;
1250
+ justify-content: center;
1251
+ }
1252
+ .video-js .vjs-fullscreen-control .vjs-icon-placeholder:before, .video-js .vjs-picture-in-picture-control .vjs-icon-placeholder:before, .video-js .vjs-subs-caps-button .vjs-icon-placeholder:before {
1253
+ position: relative;
1254
+ line-height: 1;
1255
+ font-size: 28px;
1256
+ }
1257
+ .video-js .vjs-picture-in-picture-control .vjs-icon-placeholder:before {
1258
+ font-size: 24px;
1259
+ }
1260
+ .video-js .vjs-subs-caps-button .vjs-icon-placeholder:before {
1261
+ font-size: 26px;
1262
+ }
1263
+ .video-select {
1264
+ position: absolute;
1265
+ top: 15px;
1266
+ left: 13px;
1267
+ background: transparent;
1268
+ display: none;
1269
+ font-size: 15px;
1270
+ max-width: calc(100% - 25px);
1271
+ overflow: hidden;
1272
+ text-overflow: ellipsis;
1273
+ }
1274
+ .vjs-has-started.vjs-controls-enabled .video-select {
1275
+ display: block;
1276
+ }
1277
+ .video-js .vjs-progress-holder {
1278
+ height: 6px;
1279
+ transition: 0.3s all;
1280
+ }
1281
+ .video-js .vjs-play-progress:before {
1282
+ font-size: 17px;
1283
+ transition: 0.3s all;
1284
+ }
1285
+ .video-js .vjs-progress-control:hover .vjs-progress-holder {
1286
+ height: 8px;
1287
+ }
1288
+ .vjs-progress-control:hover .vjs-play-progress:before {
1289
+ font-size: 22px;
1290
+ }
1291
+ @media (max-width: 768px) {
1292
+ .video-js .vjs-control-bar {
1293
+ padding: 0 4px 5px;
1294
+ }
1295
+ .vjs-progress-control {
1296
+ bottom: 31px !important;
1297
+ width: calc(100% - 6px) !important;
1298
+ padding: 0px !important;
1299
+ height: 30px !important;
1300
+ }
1301
+ .video-js .vjs-play-control .vjs-icon-placeholder:before {
1302
+ font-size: 25px;
1303
+ }
1304
+ .video-js .vjs-play-control {
1305
+ width: 26px;
1306
+ height: 26px;
1307
+ }
1308
+ .video-js .vjs-volume-bar {
1309
+ margin: 1.15em 0.45em;
1310
+ }
1311
+ .video-js .vjs-volume-panel .vjs-volume-control {
1312
+ width: 45px !important;
1313
+ height: 26px;
1314
+ }
1315
+ .video-js .vjs-mute-control {
1316
+ width: 26px;
1317
+ height: 26px;
1318
+ }
1319
+ .video-js .vjs-volume-panel {
1320
+ width: 80px !important;
1321
+ height: 26px;
1322
+ margin-left: 4px;
1323
+ }
1324
+ .video-js .vjs-time-control {
1325
+ padding: 0 4px;
1326
+ font-size: 12px;
1327
+ height: 26px;
1328
+ }
1329
+ .video-js .vjs-time-control.vjs-time-divider {
1330
+ font-size: 15px;
1331
+ }
1332
+ .video-js .vjs-fullscreen-control, .video-js .vjs-picture-in-picture-control, .video-js .vjs-subs-caps-button {
1333
+ height: 26px;
1334
+ width: 26px;
1335
+ }
1336
+ .video-js .vjs-fullscreen-control .vjs-icon-placeholder:before, .video-js .vjs-picture-in-picture-control .vjs-icon-placeholder:before, .video-js .vjs-subs-caps-button .vjs-icon-placeholder:before {
1337
+ font-size: 20px;
1338
+ }
1339
+ .video-js .vjs-picture-in-picture-control .vjs-icon-placeholder:before {
1340
+ font-size: 18px;
1341
+ }
1342
+ }
1343
+ @property --tw-translate-x {
1344
+ syntax: "*";
1345
+ inherits: false;
1346
+ initial-value: 0;
1347
+ }
1348
+ @property --tw-translate-y {
1349
+ syntax: "*";
1350
+ inherits: false;
1351
+ initial-value: 0;
1352
+ }
1353
+ @property --tw-translate-z {
1354
+ syntax: "*";
1355
+ inherits: false;
1356
+ initial-value: 0;
1357
+ }
1358
+ @property --tw-divide-y-reverse {
1359
+ syntax: "*";
1360
+ inherits: false;
1361
+ initial-value: 0;
1362
+ }
1363
+ @property --tw-border-style {
1364
+ syntax: "*";
1365
+ inherits: false;
1366
+ initial-value: solid;
1367
+ }
1368
+ @property --tw-gradient-position {
1369
+ syntax: "*";
1370
+ inherits: false;
1371
+ }
1372
+ @property --tw-gradient-from {
1373
+ syntax: "<color>";
1374
+ inherits: false;
1375
+ initial-value: #0000;
1376
+ }
1377
+ @property --tw-gradient-via {
1378
+ syntax: "<color>";
1379
+ inherits: false;
1380
+ initial-value: #0000;
1381
+ }
1382
+ @property --tw-gradient-to {
1383
+ syntax: "<color>";
1384
+ inherits: false;
1385
+ initial-value: #0000;
1386
+ }
1387
+ @property --tw-gradient-stops {
1388
+ syntax: "*";
1389
+ inherits: false;
1390
+ }
1391
+ @property --tw-gradient-via-stops {
1392
+ syntax: "*";
1393
+ inherits: false;
1394
+ }
1395
+ @property --tw-gradient-from-position {
1396
+ syntax: "<length-percentage>";
1397
+ inherits: false;
1398
+ initial-value: 0%;
1399
+ }
1400
+ @property --tw-gradient-via-position {
1401
+ syntax: "<length-percentage>";
1402
+ inherits: false;
1403
+ initial-value: 50%;
1404
+ }
1405
+ @property --tw-gradient-to-position {
1406
+ syntax: "<length-percentage>";
1407
+ inherits: false;
1408
+ initial-value: 100%;
1409
+ }
1410
+ @property --tw-leading {
1411
+ syntax: "*";
1412
+ inherits: false;
1413
+ }
1414
+ @property --tw-font-weight {
1415
+ syntax: "*";
1416
+ inherits: false;
1417
+ }
1418
+ @property --tw-shadow {
1419
+ syntax: "*";
1420
+ inherits: false;
1421
+ initial-value: 0 0 #0000;
1422
+ }
1423
+ @property --tw-shadow-color {
1424
+ syntax: "*";
1425
+ inherits: false;
1426
+ }
1427
+ @property --tw-shadow-alpha {
1428
+ syntax: "<percentage>";
1429
+ inherits: false;
1430
+ initial-value: 100%;
1431
+ }
1432
+ @property --tw-inset-shadow {
1433
+ syntax: "*";
1434
+ inherits: false;
1435
+ initial-value: 0 0 #0000;
1436
+ }
1437
+ @property --tw-inset-shadow-color {
1438
+ syntax: "*";
1439
+ inherits: false;
1440
+ }
1441
+ @property --tw-inset-shadow-alpha {
1442
+ syntax: "<percentage>";
1443
+ inherits: false;
1444
+ initial-value: 100%;
1445
+ }
1446
+ @property --tw-ring-color {
1447
+ syntax: "*";
1448
+ inherits: false;
1449
+ }
1450
+ @property --tw-ring-shadow {
1451
+ syntax: "*";
1452
+ inherits: false;
1453
+ initial-value: 0 0 #0000;
1454
+ }
1455
+ @property --tw-inset-ring-color {
1456
+ syntax: "*";
1457
+ inherits: false;
1458
+ }
1459
+ @property --tw-inset-ring-shadow {
1460
+ syntax: "*";
1461
+ inherits: false;
1462
+ initial-value: 0 0 #0000;
1463
+ }
1464
+ @property --tw-ring-inset {
1465
+ syntax: "*";
1466
+ inherits: false;
1467
+ }
1468
+ @property --tw-ring-offset-width {
1469
+ syntax: "<length>";
1470
+ inherits: false;
1471
+ initial-value: 0px;
1472
+ }
1473
+ @property --tw-ring-offset-color {
1474
+ syntax: "*";
1475
+ inherits: false;
1476
+ initial-value: #fff;
1477
+ }
1478
+ @property --tw-ring-offset-shadow {
1479
+ syntax: "*";
1480
+ inherits: false;
1481
+ initial-value: 0 0 #0000;
1482
+ }
1483
+ @property --tw-outline-style {
1484
+ syntax: "*";
1485
+ inherits: false;
1486
+ initial-value: solid;
1487
+ }
1488
+ @keyframes spin {
1489
+ to {
1490
+ transform: rotate(360deg);
1491
+ }
1492
+ }
1493
+ @layer properties {
1494
+ @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
1495
+ *, ::before, ::after, ::backdrop {
1496
+ --tw-translate-x: 0;
1497
+ --tw-translate-y: 0;
1498
+ --tw-translate-z: 0;
1499
+ --tw-divide-y-reverse: 0;
1500
+ --tw-border-style: solid;
1501
+ --tw-gradient-position: initial;
1502
+ --tw-gradient-from: #0000;
1503
+ --tw-gradient-via: #0000;
1504
+ --tw-gradient-to: #0000;
1505
+ --tw-gradient-stops: initial;
1506
+ --tw-gradient-via-stops: initial;
1507
+ --tw-gradient-from-position: 0%;
1508
+ --tw-gradient-via-position: 50%;
1509
+ --tw-gradient-to-position: 100%;
1510
+ --tw-leading: initial;
1511
+ --tw-font-weight: initial;
1512
+ --tw-shadow: 0 0 #0000;
1513
+ --tw-shadow-color: initial;
1514
+ --tw-shadow-alpha: 100%;
1515
+ --tw-inset-shadow: 0 0 #0000;
1516
+ --tw-inset-shadow-color: initial;
1517
+ --tw-inset-shadow-alpha: 100%;
1518
+ --tw-ring-color: initial;
1519
+ --tw-ring-shadow: 0 0 #0000;
1520
+ --tw-inset-ring-color: initial;
1521
+ --tw-inset-ring-shadow: 0 0 #0000;
1522
+ --tw-ring-inset: initial;
1523
+ --tw-ring-offset-width: 0px;
1524
+ --tw-ring-offset-color: #fff;
1525
+ --tw-ring-offset-shadow: 0 0 #0000;
1526
+ --tw-outline-style: solid;
1527
+ }
1528
+ }
1529
+ }
client/assets/videojs.hotkeys.min.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ /* videojs-hotkeys v0.2.30 - https://github.com/ctd1500/videojs-hotkeys */
2
+ (t=>{var e;"undefined"!=typeof window&&window.videojs?t(window.videojs):"function"==typeof define&&define.amd?define("videojs-hotkeys",["video.js"],function(e){return t(e.default||e)}):"undefined"!=typeof module&&module.exports&&(e=require("video.js"),module.exports=t(e.default||e))})(function(I){"undefined"!=typeof window&&(window.videojs_hotkeys={version:"0.2.30"});(I.registerPlugin||I.plugin)("hotkeys",function(a){function e(e){var t;t=u?0:y.activeElement,s.controls()&&(q||t==m||t==m.querySelector(".vjs-tech")||t==m.querySelector(".iframeblocker")||t==m.querySelector(".vjs-control-bar")||x)&&r&&(e=window.event||e,t=Math.max(-1,Math.min(1,e.wheelDelta||-e.detail)),e.preventDefault(),1==t?s.volume(s.volume()+k):-1==t&&s.volume(s.volume()-k))}var t,s=this,m=s.el(),y=document,d=1,v=2,f=3,p=4,b=5,h=6,w=7,n=I.obj?.merge||I.mergeOptions||I.util.mergeOptions,k=(a=n({volumeStep:.1,seekStep:5,enableMute:!0,enableVolumeScroll:!0,enableHoverScroll:!1,enableFullscreen:!0,enableNumbers:!0,enableJogStyle:!1,alwaysCaptureHotkeys:!1,captureDocumentHotkeys:!1,documentHotkeysFocusElementFilter:()=>!1,enableModifiersForNumbers:!0,enableInactiveFocus:!0,skipInitialFocus:!1,playPauseKey:function(e){return 32===e.which||179===e.which},rewindKey:function(e){return 37===e.which||177===e.which},forwardKey:function(e){return 39===e.which||176===e.which},volumeUpKey:function(e){return 38===e.which},volumeDownKey:function(e){return 40===e.which},muteKey:function(e){return 77===e.which},fullscreenKey:function(e){return 70===e.which},customKeys:{}},a||{})).volumeStep,o=a.seekStep,S=a.enableMute,r=a.enableVolumeScroll,u=a.enableHoverScroll,K=a.enableFullscreen,F=a.enableNumbers,j=a.enableJogStyle,q=a.alwaysCaptureHotkeys,E=a.captureDocumentHotkeys,T=a.documentHotkeysFocusElementFilter,g=a.enableModifiersForNumbers,n=a.enableInactiveFocus,l=a.skipInitialFocus,i=I.VERSION,c=(m.hasAttribute("tabIndex")||m.setAttribute("tabIndex","-1"),m.style.outline="none",!q&&s.autoplay()||l||s.one("play",()=>{m.focus()}),n&&s.on("userinactive",()=>{var o=()=>{clearTimeout(e)},e=setTimeout(()=>{s.off("useractive",o);var e=y.activeElement,t=e.parentElement,n=m.querySelector(".vjs-control-bar");!e||n!=t&&n!=t.parentElement||m.focus()},10);s.one("useractive",o)}),s.on("play",()=>{var e=m.querySelector(".iframeblocker");e&&""===e.style.display&&(e.style.display="block",e.style.bottom="39px")}),function(e){var t=e.which,n=e.preventDefault.bind(e),o=s.duration();if(s.controls()){var r,u,l,i=y.activeElement;if(q||E&&T(i)||i==m||i==m.querySelector(".vjs-tech")||i==m.querySelector(".vjs-control-bar")||i==m.querySelector(".iframeblocker"))switch(D(e,s)){case d:n(),(q||E)&&e.stopPropagation(),s.paused()?M(s.play()):s.pause();break;case v:r=!s.paused(),n(),r&&s.pause(),u=s.currentTime()-H(e),s.currentTime(u=u<=0?0:u),r&&M(s.play());break;case f:r=!s.paused(),n(),r&&s.pause(),u=s.currentTime()+H(e),s.currentTime(u=o<=u?r?o-.001:o:u),r&&M(s.play());break;case b:n(),j?(u=s.currentTime()-1,s.currentTime()<=1&&(u=0),s.currentTime(u)):s.volume(s.volume()-k);break;case p:n(),j?(u=s.currentTime()+1,s.currentTime(u=o<=u?o:u)):s.volume(s.volume()+k);break;case h:S&&s.muted(!s.muted());break;case w:K&&(s.isFullscreen()?s.exitFullscreen():s.requestFullscreen());break;default:for(l in!(47<t&&t<59||95<t&&t<106)||!g&&(e.metaKey||e.ctrlKey||e.altKey)||F&&(r=48,u=t-(r=95<t?96:r),n(),s.currentTime(s.duration()*u*.1)),a.customKeys){var c=a.customKeys[l];c&&c.key&&c.handler&&c.key(e)&&(n(),c.handler(s,a,e))}}}}),x=!1,l=m.querySelector(".vjs-volume-menu-button")||m.querySelector(".vjs-volume-panel"),D=(null!=l&&(l.addEventListener("mouseover",()=>{x=!0}),l.addEventListener("mouseout",()=>{x=!1})),function(e,t){return a.playPauseKey(e,t)?d:a.rewindKey(e,t)?v:a.forwardKey(e,t)?f:a.volumeUpKey(e,t)?p:a.volumeDownKey(e,t)?b:a.muteKey(e,t)?h:a.fullscreenKey(e,t)?w:void 0});function H(e){return"function"==typeof o?o(e):o}function M(e){null!=e&&"function"==typeof e.then&&e.then(null,function(e){})}return E?(t=function(e){c(e)},document.addEventListener("keydown",t),this.dispose=()=>{document.removeEventListener("keydown",t)}):s.on("keydown",c),s.on("dblclick",function(e){null!=i&&i<="7.1.0"&&(!s.controls()||(e=e.relatedTarget||e.toElement||y.activeElement)!=m&&e!=m.querySelector(".vjs-tech")&&e!=m.querySelector(".iframeblocker")||K&&(s.isFullscreen()?s.exitFullscreen():s.requestFullscreen()))}),s.on("mousewheel",e),s.on("DOMMouseScroll",e),this})});
client/index.html ADDED
@@ -0,0 +1,545 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>BitPlay - Stream Torrents Inside the Browser</title>
7
+ <meta
8
+ name="description"
9
+ content="BitPlay is a web-based torrent streaming application that allows you to stream torrents directly in your browser."
10
+ />
11
+ <link rel="icon" href="assets/favicon.png" type="image/png" />
12
+ <link
13
+ href="https://vjs.zencdn.net/8.10.0/video-js.min.css"
14
+ rel="stylesheet"
15
+ />
16
+ <link rel="stylesheet" href="assets/butterup.min.css" />
17
+ <link rel="stylesheet" href="assets/output.css" />
18
+ <script>
19
+ // set theme mode
20
+ if (localStorage.getItem("theme") === "dark") {
21
+ document.documentElement.classList.add("dark");
22
+ } else {
23
+ document.documentElement.classList.add("light");
24
+ }
25
+ </script>
26
+ </head>
27
+
28
+ <body>
29
+ <main
30
+ class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-10 flex flex-col items-center"
31
+ >
32
+ <div class="flex justify-between w-full gap-4 mb-8 md:mb-15">
33
+ <div class="flex items-center gap-3">
34
+ <img src="assets/bitplay_logo.png" alt="logo" class="h-12 w-auto" />
35
+ <span class="text-[24px] font-semibold">bitplay</span>
36
+ </div>
37
+ <div class="flex items-center gap-4">
38
+ <button
39
+ class="inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg:not([class*='size-'])]:size-4 shrink-0 [&amp;_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 size-10"
40
+ type="button"
41
+ id="toggle_theme"
42
+ ></button>
43
+ <button
44
+ class="inline-flex h-10 px-3 items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg:not([class*='size-'])]:size-4 shrink-0 [&amp;_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50"
45
+ type="button"
46
+ id="settings-btn"
47
+ >
48
+ <svg
49
+ xmlns="http://www.w3.org/2000/svg"
50
+ width="24"
51
+ height="24"
52
+ viewBox="0 0 24 24"
53
+ fill="none"
54
+ stroke="currentColor"
55
+ stroke-width="2"
56
+ stroke-linecap="round"
57
+ stroke-linejoin="round"
58
+ class="lucide lucide-settings2 w-4 h-4"
59
+ >
60
+ <path d="M20 7h-9"></path>
61
+ <path d="M14 17H5"></path>
62
+ <circle cx="17" cy="17" r="3"></circle>
63
+ <circle cx="7" cy="7" r="3"></circle>
64
+ </svg>
65
+ Settings
66
+ </button>
67
+ </div>
68
+ </div>
69
+ <h1
70
+ class="mb-8 text-3xl leading-[1.2] md:text-5xl max-w-xl text-center font-semibold bg-gradient-to-r from-primary to-[#00CCFF] text-transparent bg-clip-text"
71
+ >
72
+ Stream torrents with ease
73
+ </h1>
74
+ <div id="search-wrapper" class="hidden w-full">
75
+ <form id="search-form" class="w-full flex items-center gap-3">
76
+ <input
77
+ class="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive h-12 overflow-ellipsis overflow-hidden"
78
+ placeholder="Search..."
79
+ id="search"
80
+ name="search"
81
+ type="search"
82
+ />
83
+ <button class="btn" type="submit">Search</button>
84
+ </form>
85
+ <div
86
+ id="search-result"
87
+ class="mt-4 border rounded-lg overflow-auto max-h-[500px] bg-accent/50 hidden"
88
+ >
89
+ <table class="w-full caption-bottom text-sm">
90
+ <thead>
91
+ <tr class="bg-muted text-muted-foreground border-b">
92
+ <th class="p-3 text-left">Name</th>
93
+ <th class="p-3 text-left">Indexer</th>
94
+ <th class="p-3 text-left">Size</th>
95
+ <th class="p-3 text-left">Seeders</th>
96
+ <th class="p-3 text-left w-[10px]"></th>
97
+ </tr>
98
+ </thead>
99
+ <tbody
100
+ id="search-result-body"
101
+ class="divide-y [&_td]:px-3 [&_td]:py-2 [&_td]:whitespace-nowrap [&_td]:max-w-[200px] [&_td]:overflow-hidden [&_td]:text-ellipsis"
102
+ >
103
+ <tr>
104
+ <td colspan="5" class="py-10 px-6 text-center">
105
+ No results found
106
+ </td>
107
+ </tr>
108
+ </tbody>
109
+ <tfoot class="hidden">
110
+ <tr>
111
+ <td colspan="5" class="py-10 px-6 text-center">
112
+ No results found
113
+ </td>
114
+ </tr>
115
+ </tfoot>
116
+ </table>
117
+ </div>
118
+ <div id="search-pagination" class="hidden"></div>
119
+ <div
120
+ class="w-full my-4 text-center text-sm font-semibold text-accent-foreground"
121
+ >
122
+ OR
123
+ </div>
124
+ </div>
125
+ <form id="torrent-form" class="w-full flex items-center gap-3">
126
+ <div class="relative w-full">
127
+ <input
128
+ class="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive h-12 pr-12 overflow-ellipsis overflow-hidden"
129
+ placeholder="Magnet link"
130
+ id="magnet"
131
+ name="magnet"
132
+ />
133
+ <button
134
+ id="copy_magnet"
135
+ class="inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg:not([class*='size-'])]:size-4 shrink-0 [&amp;_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 size-10 absolute right-0 top-0 h-full w-12"
136
+ type="button"
137
+ >
138
+ <svg
139
+ xmlns="http://www.w3.org/2000/svg"
140
+ width="24"
141
+ height="24"
142
+ viewBox="0 0 24 24"
143
+ fill="none"
144
+ stroke="currentColor"
145
+ stroke-width="2"
146
+ stroke-linecap="round"
147
+ stroke-linejoin="round"
148
+ class="lucide lucide-clipboard-paste size-5"
149
+ >
150
+ <path
151
+ d="M15 2H9a1 1 0 0 0-1 1v2c0 .6.4 1 1 1h6c.6 0 1-.4 1-1V3c0-.6-.4-1-1-1Z"
152
+ ></path>
153
+ <path
154
+ d="M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2M16 4h2a2 2 0 0 1 2 2v2M11 14h10"
155
+ ></path>
156
+ <path d="m17 10 4 4-4 4"></path>
157
+ </svg>
158
+ </button>
159
+ </div>
160
+ <button class="btn" type="submit">Play Now</button>
161
+ </form>
162
+ <div
163
+ id="demo_torrent"
164
+ class="w-full rounded-lg p-3 text-center flex items-center gap-2 justify-center text-sm font-semibold text-accent-foreground border-2 mt-4 border-primary bg-secondary/50 cursor-pointer hover:bg-transparent transition-all"
165
+ >
166
+ <svg
167
+ xmlns="http://www.w3.org/2000/svg"
168
+ width="24"
169
+ height="24"
170
+ viewBox="0 0 24 24"
171
+ fill="none"
172
+ stroke="currentColor"
173
+ stroke-width="2"
174
+ stroke-linecap="round"
175
+ stroke-linejoin="round"
176
+ class="lucide lucide-video w-6 h-6 inline-block"
177
+ >
178
+ <path
179
+ d="m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5"
180
+ ></path>
181
+ <rect x="2" y="6" width="14" height="12" rx="2"></rect>
182
+ </svg>
183
+ Try Demo with Sintel (CC Movie)
184
+ </div>
185
+ <div
186
+ id="torrent_file_wrapper"
187
+ class="w-full relative rounded-lg p-3 py-10 text-center flex flex-col items-center gap-4 justify-center text-sm font-semibold text-accent-foreground mt-7 border-2 border-foreground/30 border-dashed bg-secondary/50 cursor-pointer hover:bg-transparent transition-all"
188
+ >
189
+ <svg
190
+ xmlns="http://www.w3.org/2000/svg"
191
+ width="24"
192
+ height="24"
193
+ viewBox="0 0 24 24"
194
+ fill="none"
195
+ stroke="currentColor"
196
+ stroke-width="2"
197
+ stroke-linecap="round"
198
+ stroke-linejoin="round"
199
+ class="lucide lucide-upload size-12 text-primary"
200
+ >
201
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
202
+ <polyline points="17 8 12 3 7 8"></polyline>
203
+ <line x1="12" x2="12" y1="3" y2="15"></line>
204
+ </svg>
205
+ <span class="text-sm text-accent-foreground font-bold"
206
+ >Drag and drop your torrent file here, or
207
+ <span class="text-primary">Browse</span>
208
+ </span>
209
+ <input
210
+ type="file"
211
+ class="opacity-0 size-full z-[1] absolute cursor-pointer"
212
+ accept=".torrent"
213
+ id="torrent_file"
214
+ />
215
+ </div>
216
+ <video id="video-player" class="video-js mt-10 w-full"></video>
217
+ </main>
218
+ <div id="settings-model" class="fixed inset-0 z-[999] flex flex-col hidden">
219
+ <!-- model overlay -->
220
+ <div id="close-settings" class="absolute inset-0 bg-black/70"></div>
221
+ <!-- model content -->
222
+ <div
223
+ class="p-6 bg-background rounded-xl shadow-lg w-full max-w-lg m-auto border z-[2]"
224
+ >
225
+ <div class="flex items-center justify-between mb-6">
226
+ <h1 class="text-xl font-semibold">Settings</h1>
227
+ <button id="close-settings" type="button">
228
+ <svg
229
+ xmlns="http://www.w3.org/2000/svg"
230
+ width="24"
231
+ height="24"
232
+ viewBox="0 0 24 24"
233
+ fill="none"
234
+ stroke="currentColor"
235
+ stroke-width="2"
236
+ stroke-linecap="round"
237
+ stroke-linejoin="round"
238
+ class="lucide lucide-x w-5 h-5"
239
+ >
240
+ <path d="M18 6 6 18"></path>
241
+ <path d="m6 6 12 12"></path>
242
+ </svg>
243
+ </button>
244
+ </div>
245
+ <div class="flex gap-2 mb-7">
246
+ <button
247
+ class="tab-btn flex items-center gap-2 px-4 py-2 rounded-lg transition-colors border bg-primary text-primary-foreground"
248
+ data-index="0"
249
+ type="button"
250
+ >
251
+ <svg
252
+ xmlns="http://www.w3.org/2000/svg"
253
+ width="24"
254
+ height="24"
255
+ viewBox="0 0 24 24"
256
+ fill="none"
257
+ stroke="currentColor"
258
+ stroke-width="2"
259
+ stroke-linecap="round"
260
+ stroke-linejoin="round"
261
+ class="lucide lucide-globe w-4 h-4"
262
+ >
263
+ <circle cx="12" cy="12" r="10"></circle>
264
+ <path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"></path>
265
+ <path d="M2 12h20"></path>
266
+ </svg>
267
+ <span>Proxy</span>
268
+ </button>
269
+ <button
270
+ class="tab-btn flex items-center gap-2 px-4 py-2 rounded-lg transition-colors bg-muted border"
271
+ data-index="1"
272
+ type="button"
273
+ >
274
+ <svg
275
+ xmlns="http://www.w3.org/2000/svg"
276
+ width="24"
277
+ height="24"
278
+ viewBox="0 0 24 24"
279
+ fill="none"
280
+ stroke="currentColor"
281
+ stroke-width="2"
282
+ stroke-linecap="round"
283
+ stroke-linejoin="round"
284
+ class="lucide lucide-search w-4 h-4"
285
+ >
286
+ <circle cx="11" cy="11" r="8"></circle>
287
+ <path d="m21 21-4.3-4.3"></path>
288
+ </svg>
289
+ <span>Prowlarr</span>
290
+ </button>
291
+ <button
292
+ class="tab-btn flex items-center gap-2 px-4 py-2 rounded-lg transition-colors bg-muted border"
293
+ data-index="2"
294
+ type="button"
295
+ >
296
+ <svg
297
+ xmlns="http://www.w3.org/2000/svg"
298
+ width="24"
299
+ height="24"
300
+ viewBox="0 0 24 24"
301
+ fill="none"
302
+ stroke="currentColor"
303
+ stroke-width="2"
304
+ stroke-linecap="round"
305
+ stroke-linejoin="round"
306
+ class="lucide lucide-server w-4 h-4"
307
+ >
308
+ <rect width="20" height="8" x="2" y="2" rx="2" ry="2"></rect>
309
+ <rect width="20" height="8" x="2" y="14" rx="2" ry="2"></rect>
310
+ <line x1="6" x2="6.01" y1="6" y2="6"></line>
311
+ <line x1="6" x2="6.01" y1="18" y2="18"></line>
312
+ </svg>
313
+ <span>Jackett</span>
314
+ </button>
315
+ </div>
316
+ <form
317
+ id="proxy-settings-form"
318
+ data-tab="0"
319
+ class="tab flex flex-col gap-6"
320
+ >
321
+ <label
322
+ id="switchInput"
323
+ for="enableProxy"
324
+ class="flex items-center cursor-pointer"
325
+ >
326
+ <input id="enableProxy" type="checkbox" class="sr-only" />
327
+ <div
328
+ class="w-11 switch-wrapper h-6 border bg-accent rounded-full shadow-inner relative transition"
329
+ >
330
+ <div
331
+ class="dot absolute size-5 bg-white rounded-full shadow left-[1px] top-[1px] transition"
332
+ ></div>
333
+ </div>
334
+ <span class="ml-3 text-muted-foreground"> Enable Proxy </span>
335
+ </label>
336
+ <div class="flex flex-col gap-2">
337
+ <label
338
+ for="proxy"
339
+ class="text-muted-foreground text-sm font-semibold"
340
+ >Enter a SOCKS5 Proxy URL
341
+ </label>
342
+ <input
343
+ type="text"
344
+ id="proxyUrl"
345
+ placeholder="socks5://username:password@host:port"
346
+ class="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive h-10 pr-12 overflow-ellipsis overflow-hidden"
347
+ />
348
+ </div>
349
+ <div id="proxy-result" class="hidden items-center gap-2">
350
+ <span class="text-muted-foreground">IP Address:</span
351
+ ><span class="output-ip text-primary"></span>
352
+ </div>
353
+ <div class="flex gap-2">
354
+ <button
355
+ type="button"
356
+ id="test-proxy"
357
+ class="flex items-center justify-center gap-2 px-4 py-2 flex-1 rounded-lg transition-colors bg-muted border font-medium text-sm"
358
+ >
359
+ <svg
360
+ xmlns="http://www.w3.org/2000/svg"
361
+ width="24"
362
+ height="24"
363
+ viewBox="0 0 24 24"
364
+ fill="none"
365
+ stroke="currentColor"
366
+ stroke-width="2"
367
+ stroke-linecap="round"
368
+ stroke-linejoin="round"
369
+ class="lucide lucide-test-tube2 w-4 h-4"
370
+ >
371
+ <path
372
+ d="M21 7 6.82 21.18a2.83 2.83 0 0 1-3.99-.01v0a2.83 2.83 0 0 1 0-4L17 3"
373
+ ></path>
374
+ <path d="m16 2 6 6"></path>
375
+ <path d="M12 16H4"></path>
376
+ </svg>
377
+ <span>Test Proxy</span>
378
+ </button>
379
+ <button type="submit" class="btn flex-1 !h-11">
380
+ Save Settings
381
+ </button>
382
+ </div>
383
+ </form>
384
+ <form
385
+ id="prowlarr-settings-form"
386
+ data-tab="1"
387
+ class="tab flex flex-col gap-6 hidden"
388
+ >
389
+ <label
390
+ id="switchInput"
391
+ for="enableProwlarr"
392
+ class="flex items-center cursor-pointer"
393
+ >
394
+ <input id="enableProwlarr" type="checkbox" class="sr-only" />
395
+ <div
396
+ class="w-11 switch-wrapper h-6 border bg-accent rounded-full shadow-inner relative transition"
397
+ >
398
+ <div
399
+ class="dot absolute size-5 bg-white rounded-full shadow left-[1px] top-[1px] transition"
400
+ ></div>
401
+ </div>
402
+ <span class="ml-3 text-muted-foreground"> Enable Prowlarr </span>
403
+ </label>
404
+ <div class="flex flex-col gap-2">
405
+ <label
406
+ for="prowlarrHost"
407
+ class="text-muted-foreground text-sm font-semibold"
408
+ >Prowlarr Host</label
409
+ >
410
+ <input
411
+ type="text"
412
+ id="prowlarrHost"
413
+ placeholder="http://localhost:9117"
414
+ class="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive h-10 pr-12 overflow-ellipsis overflow-hidden"
415
+ />
416
+ </div>
417
+ <div class="flex flex-col gap-2">
418
+ <label
419
+ for="prowlarrApiKey"
420
+ class="text-muted-foreground text-sm font-semibold"
421
+ >API Key</label
422
+ >
423
+ <input
424
+ type="text"
425
+ id="prowlarrApiKey"
426
+ placeholder="Your Prowlarr API key"
427
+ class="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive h-10 pr-12 overflow-ellipsis overflow-hidden"
428
+ />
429
+ </div>
430
+ <div class="flex gap-2">
431
+ <button
432
+ type="button"
433
+ id="test-prowlarr"
434
+ class="flex items-center justify-center gap-2 px-4 py-2 flex-1 rounded-lg transition-colors bg-muted border font-medium text-sm"
435
+ >
436
+ <svg
437
+ xmlns="http://www.w3.org/2000/svg"
438
+ width="24"
439
+ height="24"
440
+ viewBox="0 0 24 24"
441
+ fill="none"
442
+ stroke="currentColor"
443
+ stroke-width="2"
444
+ stroke-linecap="round"
445
+ stroke-linejoin="round"
446
+ class="lucide lucide-test-tube2 w-4 h-4"
447
+ >
448
+ <path
449
+ d="M21 7 6.82 21.18a2.83 2.83 0 0 1-3.99-.01v0a2.83 2.83 0 0 1 0-4L17 3"
450
+ ></path>
451
+ <path d="m16 2 6 6"></path>
452
+ <path d="M12 16H4"></path>
453
+ </svg>
454
+ <span>Test Connection</span>
455
+ </button>
456
+ <button type="submit" class="btn flex-1 !h-11">
457
+ Save Settings
458
+ </button>
459
+ </div>
460
+ </form>
461
+ <form
462
+ id="jackett-settings-form"
463
+ data-tab="2"
464
+ class="tab flex flex-col gap-6 hidden"
465
+ >
466
+ <label
467
+ id="switchInput"
468
+ for="enableJackett"
469
+ class="flex items-center cursor-pointer"
470
+ >
471
+ <input id="enableJackett" type="checkbox" class="sr-only" />
472
+ <div
473
+ class="w-11 switch-wrapper h-6 border bg-accent rounded-full shadow-inner relative transition"
474
+ >
475
+ <div
476
+ class="dot absolute size-5 bg-white rounded-full shadow left-[1px] top-[1px] transition"
477
+ ></div>
478
+ </div>
479
+ <span class="ml-3 text-muted-foreground"> Enable Jackett </span>
480
+ </label>
481
+ <div class="flex flex-col gap-2">
482
+ <label
483
+ for="jackettHost"
484
+ class="text-muted-foreground text-sm font-semibold"
485
+ >Jackett Host</label
486
+ >
487
+ <input
488
+ type="text"
489
+ id="jackettHost"
490
+ placeholder="http://localhost:9117"
491
+ class="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive h-10 pr-12 overflow-ellipsis overflow-hidden"
492
+ />
493
+ </div>
494
+ <div class="flex flex-col gap-2">
495
+ <label
496
+ for="jackettApiKey"
497
+ class="text-muted-foreground text-sm font-semibold"
498
+ >API Key</label
499
+ >
500
+ <input
501
+ type="text"
502
+ id="jackettApiKey"
503
+ placeholder="Your Jackett API key"
504
+ class="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive h-10 pr-12 overflow-ellipsis overflow-hidden"
505
+ />
506
+ </div>
507
+ <div class="flex gap-2">
508
+ <button
509
+ type="button"
510
+ id="test-jackett"
511
+ class="flex items-center justify-center gap-2 px-4 py-2 flex-1 rounded-lg transition-colors bg-muted border font-medium text-sm"
512
+ >
513
+ <svg
514
+ xmlns="http://www.w3.org/2000/svg"
515
+ width="24"
516
+ height="24"
517
+ viewBox="0 0 24 24"
518
+ fill="none"
519
+ stroke="currentColor"
520
+ stroke-width="2"
521
+ stroke-linecap="round"
522
+ stroke-linejoin="round"
523
+ class="lucide lucide-test-tube2 w-4 h-4"
524
+ >
525
+ <path
526
+ d="M21 7 6.82 21.18a2.83 2.83 0 0 1-3.99-.01v0a2.83 2.83 0 0 1 0-4L17 3"
527
+ ></path>
528
+ <path d="m16 2 6 6"></path>
529
+ <path d="M12 16H4"></path>
530
+ </svg>
531
+ <span>Test Connection</span>
532
+ </button>
533
+ <button type="submit" class="btn flex-1 !h-11">
534
+ Save Settings
535
+ </button>
536
+ </div>
537
+ </form>
538
+ </div>
539
+ </div>
540
+ <script src="https://vjs.zencdn.net/8.10.0/video.min.js"></script>
541
+ <script src="assets/videojs.hotkeys.min.js"></script>
542
+ <script src="assets/butterup.min.js"></script>
543
+ <script type="module" src="assets/index.js"></script>
544
+ </body>
545
+ </html>
go.mod ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module torrent-stream
2
+
3
+ go 1.24.0
4
+
5
+ require (
6
+ github.com/anacrolix/torrent v1.58.1
7
+ golang.org/x/net v0.38.0
8
+ )
9
+
10
+ require (
11
+ github.com/RoaringBitmap/roaring v1.2.3 // indirect
12
+ github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
13
+ github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
14
+ github.com/anacrolix/chansync v0.4.1-0.20240627045151-1aa1ac392fe8 // indirect
15
+ github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 // indirect
16
+ github.com/anacrolix/envpprof v1.3.0 // indirect
17
+ github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca // indirect
18
+ github.com/anacrolix/go-libutp v1.3.2 // indirect
19
+ github.com/anacrolix/log v0.15.3-0.20240627045001-cd912c641d83 // indirect
20
+ github.com/anacrolix/missinggo v1.3.0 // indirect
21
+ github.com/anacrolix/missinggo/perf v1.0.0 // indirect
22
+ github.com/anacrolix/missinggo/v2 v2.7.4 // indirect
23
+ github.com/anacrolix/mmsg v1.0.1 // indirect
24
+ github.com/anacrolix/multiless v0.4.0 // indirect
25
+ github.com/anacrolix/stm v0.4.0 // indirect
26
+ github.com/anacrolix/sync v0.5.1 // indirect
27
+ github.com/anacrolix/upnp v0.1.4 // indirect
28
+ github.com/anacrolix/utp v0.1.0 // indirect
29
+ github.com/bahlo/generic-list-go v0.2.0 // indirect
30
+ github.com/benbjohnson/immutable v0.3.0 // indirect
31
+ github.com/bits-and-blooms/bitset v1.2.2 // indirect
32
+ github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
33
+ github.com/cespare/xxhash v1.1.0 // indirect
34
+ github.com/davecgh/go-spew v1.1.1 // indirect
35
+ github.com/dustin/go-humanize v1.0.0 // indirect
36
+ github.com/edsrzf/mmap-go v1.1.0 // indirect
37
+ github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 // indirect
38
+ github.com/go-llsqlite/crawshaw v0.5.2-0.20240425034140-f30eb7704568 // indirect
39
+ github.com/go-logr/logr v1.2.3 // indirect
40
+ github.com/go-logr/stdr v1.2.2 // indirect
41
+ github.com/google/btree v1.1.2 // indirect
42
+ github.com/google/uuid v1.6.0 // indirect
43
+ github.com/gorilla/websocket v1.5.0 // indirect
44
+ github.com/huandu/xstrings v1.3.2 // indirect
45
+ github.com/klauspost/cpuid/v2 v2.2.3 // indirect
46
+ github.com/mattn/go-isatty v0.0.16 // indirect
47
+ github.com/minio/sha256-simd v1.0.0 // indirect
48
+ github.com/mr-tron/base58 v1.2.0 // indirect
49
+ github.com/mschoch/smat v0.2.0 // indirect
50
+ github.com/multiformats/go-multihash v0.2.3 // indirect
51
+ github.com/multiformats/go-varint v0.0.6 // indirect
52
+ github.com/pion/datachannel v1.5.9 // indirect
53
+ github.com/pion/dtls/v3 v3.0.3 // indirect
54
+ github.com/pion/ice/v4 v4.0.2 // indirect
55
+ github.com/pion/interceptor v0.1.37 // indirect
56
+ github.com/pion/logging v0.2.2 // indirect
57
+ github.com/pion/mdns/v2 v2.0.7 // indirect
58
+ github.com/pion/randutil v0.1.0 // indirect
59
+ github.com/pion/rtcp v1.2.14 // indirect
60
+ github.com/pion/rtp v1.8.9 // indirect
61
+ github.com/pion/sctp v1.8.33 // indirect
62
+ github.com/pion/sdp/v3 v3.0.9 // indirect
63
+ github.com/pion/srtp/v3 v3.0.4 // indirect
64
+ github.com/pion/stun/v3 v3.0.0 // indirect
65
+ github.com/pion/transport/v3 v3.0.7 // indirect
66
+ github.com/pion/turn/v4 v4.0.0 // indirect
67
+ github.com/pion/webrtc/v4 v4.0.0 // indirect
68
+ github.com/pkg/errors v0.9.1 // indirect
69
+ github.com/protolambda/ctxlock v0.1.0 // indirect
70
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
71
+ github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
72
+ github.com/spaolacci/murmur3 v1.1.0 // indirect
73
+ github.com/tidwall/btree v1.6.0 // indirect
74
+ github.com/wlynxg/anet v0.0.3 // indirect
75
+ go.etcd.io/bbolt v1.3.6 // indirect
76
+ go.opentelemetry.io/otel v1.11.1 // indirect
77
+ go.opentelemetry.io/otel/trace v1.11.1 // indirect
78
+ golang.org/x/crypto v0.36.0 // indirect
79
+ golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
80
+ golang.org/x/sync v0.8.0 // indirect
81
+ golang.org/x/sys v0.31.0 // indirect
82
+ golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
83
+ lukechampine.com/blake3 v1.1.6 // indirect
84
+ modernc.org/libc v1.22.3 // indirect
85
+ modernc.org/mathutil v1.5.0 // indirect
86
+ modernc.org/memory v1.5.0 // indirect
87
+ modernc.org/sqlite v1.21.1 // indirect
88
+ zombiezen.com/go/sqlite v0.13.1 // indirect
89
+ )
go.sum ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2
+ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3
+ crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk=
4
+ crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
5
+ filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
6
+ filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
7
+ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
8
+ github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
9
+ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
10
+ github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
11
+ github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
12
+ github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
13
+ github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
14
+ github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
15
+ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
16
+ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
17
+ github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0=
18
+ github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0/go.mod h1:q37NoqncT41qKc048STsifIt69LfUJ8SrWWcz/yam5k=
19
+ github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk=
20
+ github.com/alecthomas/assert/v2 v2.0.0-alpha3/go.mod h1:+zD0lmDXTeQj7TgDgCt0ePWxb0hMC1G+PGTsTCv1B9o=
21
+ github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8=
22
+ github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI=
23
+ github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
24
+ github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
25
+ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
26
+ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
27
+ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
28
+ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
29
+ github.com/anacrolix/chansync v0.4.1-0.20240627045151-1aa1ac392fe8 h1:eyb0bBaQKMOh5Se/Qg54shijc8K4zpQiOjEhKFADkQM=
30
+ github.com/anacrolix/chansync v0.4.1-0.20240627045151-1aa1ac392fe8/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
31
+ github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 h1:8V0K09lrGoeT2KRJNOtspA7q+OMxGwQqK/Ug0IiaaRE=
32
+ github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444/go.mod h1:MctKM1HS5YYDb3F30NGJxLE+QPuqWoT5ReW/4jt8xew=
33
+ github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
34
+ github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
35
+ github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
36
+ github.com/anacrolix/envpprof v1.3.0 h1:WJt9bpuT7A/CDCxPOv/eeZqHWlle/Y0keJUvc6tcJDk=
37
+ github.com/anacrolix/envpprof v1.3.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0=
38
+ github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
39
+ github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca h1:aiiGqSQWjtVNdi8zUMfA//IrM8fPkv2bWwZVPbDe0wg=
40
+ github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca/go.mod h1:MN3ve08Z3zSV/rTuX/ouI4lNdlfTxgdafQJiLzyNRB8=
41
+ github.com/anacrolix/go-libutp v1.3.2 h1:WswiaxTIogchbkzNgGHuHRfbrYLpv4o290mlvcx+++M=
42
+ github.com/anacrolix/go-libutp v1.3.2/go.mod h1:fCUiEnXJSe3jsPG554A200Qv+45ZzIIyGEvE56SHmyA=
43
+ github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
44
+ github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
45
+ github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68=
46
+ github.com/anacrolix/log v0.14.2/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
47
+ github.com/anacrolix/log v0.15.3-0.20240627045001-cd912c641d83 h1:9o/yVzzLzYaBDFx8B27yhkvBLhNnRAuSTK7Y+yZKVtU=
48
+ github.com/anacrolix/log v0.15.3-0.20240627045001-cd912c641d83/go.mod h1:xvHjsYWWP7yO8PZwtuIp/k0DBlu07pSJqH4SEC78Vwc=
49
+ github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62 h1:P04VG6Td13FHMgS5ZBcJX23NPC/fiC4cp9bXwYujdYM=
50
+ github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM=
51
+ github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s=
52
+ github.com/anacrolix/missinggo v1.1.0/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo=
53
+ github.com/anacrolix/missinggo v1.1.2-0.20190815015349-b888af804467/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo=
54
+ github.com/anacrolix/missinggo v1.2.1/go.mod h1:J5cMhif8jPmFoC3+Uvob3OXXNIhOUikzMt+uUjeM21Y=
55
+ github.com/anacrolix/missinggo v1.3.0 h1:06HlMsudotL7BAELRZs0yDZ4yVXsHXGi323QBjAVASw=
56
+ github.com/anacrolix/missinggo v1.3.0/go.mod h1:bqHm8cE8xr+15uVfMG3BFui/TxyB6//H5fwlq/TeqMc=
57
+ github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy6BqESAJVw=
58
+ github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ=
59
+ github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY=
60
+ github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA=
61
+ github.com/anacrolix/missinggo/v2 v2.7.4 h1:47h5OXoPV8JbA/ACA+FLwKdYbAinuDO8osc2Cu9xkxg=
62
+ github.com/anacrolix/missinggo/v2 v2.7.4/go.mod h1:vVO5FEziQm+NFmJesc7StpkquZk+WJFCaL0Wp//2sa0=
63
+ github.com/anacrolix/mmsg v1.0.1 h1:TxfpV7kX70m3f/O7ielL/2I3OFkMPjrRCPo7+4X5AWw=
64
+ github.com/anacrolix/mmsg v1.0.1/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc=
65
+ github.com/anacrolix/multiless v0.4.0 h1:lqSszHkliMsZd2hsyrDvHOw4AbYWa+ijQ66LzbjqWjM=
66
+ github.com/anacrolix/multiless v0.4.0/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM=
67
+ github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg=
68
+ github.com/anacrolix/stm v0.4.0 h1:tOGvuFwaBjeu1u9X1eIh9TX8OEedEiEQ1se1FjhFnXY=
69
+ github.com/anacrolix/stm v0.4.0/go.mod h1:GCkwqWoAsP7RfLW+jw+Z0ovrt2OO7wRzcTtFYMYY5t8=
70
+ github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk=
71
+ github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
72
+ github.com/anacrolix/sync v0.5.1 h1:FbGju6GqSjzVoTgcXTUKkF041lnZkG5P0C3T5RL3SGc=
73
+ github.com/anacrolix/sync v0.5.1/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
74
+ github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
75
+ github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
76
+ github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
77
+ github.com/anacrolix/torrent v1.58.1 h1:6FP+KH57b1gyT2CpVL9fEqf9MGJEgh3xw1VA8rI0pW8=
78
+ github.com/anacrolix/torrent v1.58.1/go.mod h1:/7ZdLuHNKgtCE1gjYJCfbtG9JodBcDaF5ip5EUWRtk8=
79
+ github.com/anacrolix/upnp v0.1.4 h1:+2t2KA6QOhm/49zeNyeVwDu1ZYS9dB9wfxyVvh/wk7U=
80
+ github.com/anacrolix/upnp v0.1.4/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
81
+ github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4=
82
+ github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk=
83
+ github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
84
+ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
85
+ github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
86
+ github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
87
+ github.com/benbjohnson/immutable v0.3.0 h1:TVRhuZx2wG9SZ0LRdqlbs9S5BZ6Y24hJEHTCgWHZEIw=
88
+ github.com/benbjohnson/immutable v0.3.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
89
+ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
90
+ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
91
+ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
92
+ github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
93
+ github.com/bits-and-blooms/bitset v1.2.2 h1:J5gbX05GpMdBjCvQ9MteIg2KKDExr7DrgK+Yc15FvIk=
94
+ github.com/bits-and-blooms/bitset v1.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
95
+ github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
96
+ github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
97
+ github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
98
+ github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og=
99
+ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
100
+ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
101
+ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
102
+ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
103
+ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
104
+ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
105
+ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
106
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
107
+ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
108
+ github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
109
+ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
110
+ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
111
+ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
112
+ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
113
+ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
114
+ github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
115
+ github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
116
+ github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
117
+ github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
118
+ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
119
+ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
120
+ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
121
+ github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
122
+ github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
123
+ github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
124
+ github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
125
+ github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
126
+ github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
127
+ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
128
+ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
129
+ github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 h1:OyQmpAN302wAopDgwVjgs2HkFawP9ahIEqkUYz7V7CA=
130
+ github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916/go.mod h1:DADrR88ONKPPeSGjFp5iEN55Arx3fi2qXZeKCYDpbmU=
131
+ github.com/go-llsqlite/crawshaw v0.5.2-0.20240425034140-f30eb7704568 h1:3EpZo8LxIzF4q3BT+vttQQlRfA6uTtTb/cxVisWa5HM=
132
+ github.com/go-llsqlite/crawshaw v0.5.2-0.20240425034140-f30eb7704568/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
133
+ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
134
+ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
135
+ github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
136
+ github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
137
+ github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
138
+ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
139
+ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
140
+ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
141
+ github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
142
+ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
143
+ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
144
+ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
145
+ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
146
+ github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
147
+ github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
148
+ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
149
+ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
150
+ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
151
+ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
152
+ github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
153
+ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
154
+ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
155
+ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
156
+ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
157
+ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
158
+ github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
159
+ github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
160
+ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
161
+ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
162
+ github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
163
+ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
164
+ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
165
+ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
166
+ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
167
+ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
168
+ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
169
+ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
170
+ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
171
+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
172
+ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
173
+ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
174
+ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
175
+ github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
176
+ github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
177
+ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
178
+ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
179
+ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
180
+ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
181
+ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
182
+ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
183
+ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
184
+ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
185
+ github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
186
+ github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
187
+ github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
188
+ github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
189
+ github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
190
+ github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
191
+ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
192
+ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
193
+ github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
194
+ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
195
+ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
196
+ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
197
+ github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
198
+ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
199
+ github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
200
+ github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
201
+ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
202
+ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
203
+ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
204
+ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
205
+ github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
206
+ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
207
+ github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
208
+ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
209
+ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
210
+ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
211
+ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
212
+ github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
213
+ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
214
+ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
215
+ github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
216
+ github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
217
+ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
218
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
219
+ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
220
+ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
221
+ github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
222
+ github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
223
+ github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
224
+ github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
225
+ github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
226
+ github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
227
+ github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
228
+ github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY=
229
+ github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
230
+ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
231
+ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
232
+ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
233
+ github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
234
+ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
235
+ github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
236
+ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
237
+ github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
238
+ github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE=
239
+ github.com/pion/dtls/v3 v3.0.3 h1:j5ajZbQwff7Z8k3pE3S+rQ4STvKvXUdKsi/07ka+OWM=
240
+ github.com/pion/dtls/v3 v3.0.3/go.mod h1:weOTUyIV4z0bQaVzKe8kpaP17+us3yAuiQsEAG1STMU=
241
+ github.com/pion/ice/v4 v4.0.2 h1:1JhBRX8iQLi0+TfcavTjPjI6GO41MFn4CeTBX+Y9h5s=
242
+ github.com/pion/ice/v4 v4.0.2/go.mod h1:DCdqyzgtsDNYN6/3U8044j3U7qsJ9KFJC92VnOWHvXg=
243
+ github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
244
+ github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
245
+ github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
246
+ github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
247
+ github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
248
+ github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
249
+ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
250
+ github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
251
+ github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
252
+ github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
253
+ github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
254
+ github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
255
+ github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
256
+ github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
257
+ github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
258
+ github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
259
+ github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
260
+ github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
261
+ github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
262
+ github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
263
+ github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
264
+ github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
265
+ github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
266
+ github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
267
+ github.com/pion/webrtc/v4 v4.0.0 h1:x8ec7uJQPP3D1iI8ojPAiTOylPI7Fa7QgqZrhpLyqZ8=
268
+ github.com/pion/webrtc/v4 v4.0.0/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto=
269
+ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
270
+ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
271
+ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
272
+ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
273
+ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
274
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
275
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
276
+ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
277
+ github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
278
+ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
279
+ github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
280
+ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
281
+ github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
282
+ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
283
+ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
284
+ github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
285
+ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
286
+ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
287
+ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
288
+ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
289
+ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
290
+ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
291
+ github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
292
+ github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE=
293
+ github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
294
+ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
295
+ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
296
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
297
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
298
+ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
299
+ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
300
+ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
301
+ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
302
+ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
303
+ github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs=
304
+ github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
305
+ github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
306
+ github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
307
+ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
308
+ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
309
+ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
310
+ github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
311
+ github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
312
+ github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
313
+ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
314
+ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
315
+ github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
316
+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
317
+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
318
+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
319
+ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
320
+ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
321
+ github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
322
+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
323
+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
324
+ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
325
+ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
326
+ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
327
+ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
328
+ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
329
+ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
330
+ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
331
+ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
332
+ github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg=
333
+ github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
334
+ github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
335
+ github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
336
+ github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
337
+ github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
338
+ github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
339
+ github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg=
340
+ github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
341
+ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
342
+ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
343
+ go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
344
+ go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
345
+ go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
346
+ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
347
+ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
348
+ go.opentelemetry.io/otel v1.11.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4=
349
+ go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE=
350
+ go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
351
+ go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk=
352
+ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
353
+ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
354
+ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
355
+ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
356
+ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
357
+ golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
358
+ golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
359
+ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
360
+ golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
361
+ golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
362
+ golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
363
+ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
364
+ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
365
+ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
366
+ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
367
+ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
368
+ golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
369
+ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
370
+ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
371
+ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
372
+ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
373
+ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
374
+ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
375
+ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
376
+ golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
377
+ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
378
+ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
379
+ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
380
+ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
381
+ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
382
+ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
383
+ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
384
+ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
385
+ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
386
+ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
387
+ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
388
+ golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
389
+ golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
390
+ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
391
+ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
392
+ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
393
+ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
394
+ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
395
+ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
396
+ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
397
+ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
398
+ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
399
+ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
400
+ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
401
+ golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
402
+ golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
403
+ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
404
+ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
405
+ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
406
+ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
407
+ golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
408
+ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
409
+ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
410
+ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
411
+ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
412
+ golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
413
+ golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
414
+ golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
415
+ golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
416
+ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
417
+ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
418
+ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
419
+ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
420
+ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
421
+ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
422
+ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
423
+ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
424
+ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
425
+ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
426
+ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
427
+ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
428
+ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
429
+ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
430
+ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
431
+ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
432
+ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
433
+ golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
434
+ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
435
+ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
436
+ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
437
+ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
438
+ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
439
+ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
440
+ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
441
+ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
442
+ golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
443
+ golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
444
+ golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
445
+ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
446
+ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
447
+ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
448
+ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
449
+ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
450
+ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
451
+ golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
452
+ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
453
+ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
454
+ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
455
+ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
456
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
457
+ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
458
+ google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
459
+ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
460
+ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
461
+ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
462
+ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
463
+ google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
464
+ google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
465
+ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
466
+ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
467
+ google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
468
+ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
469
+ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
470
+ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
471
+ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
472
+ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
473
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
474
+ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
475
+ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
476
+ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
477
+ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
478
+ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
479
+ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
480
+ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
481
+ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
482
+ gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
483
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
484
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
485
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
486
+ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
487
+ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
488
+ lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c=
489
+ lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
490
+ modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
491
+ modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
492
+ modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
493
+ modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
494
+ modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
495
+ modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
496
+ modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU=
497
+ modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
498
+ zombiezen.com/go/sqlite v0.13.1 h1:qDzxyWWmMtSSEH5qxamqBFmqA2BLSSbtODi3ojaE02o=
499
+ zombiezen.com/go/sqlite v0.13.1/go.mod h1:Ht/5Rg3Ae2hoyh1I7gbWtWAl89CNocfqeb/aAMTkJr4=
main.go ADDED
@@ -0,0 +1,1434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "errors"
8
+ "fmt"
9
+ "io"
10
+ "log"
11
+ "math/rand"
12
+ "net"
13
+ "net/http"
14
+ "os"
15
+ "reflect"
16
+ "runtime"
17
+ "strconv"
18
+ "strings"
19
+ "sync"
20
+ "time"
21
+
22
+ "net/url"
23
+ "path/filepath"
24
+
25
+ "github.com/anacrolix/torrent"
26
+ "github.com/anacrolix/torrent/metainfo"
27
+ "github.com/anacrolix/torrent/storage"
28
+ "golang.org/x/net/proxy"
29
+ )
30
+
31
+ var (
32
+ currentSettings Settings
33
+ settingsMutex sync.RWMutex
34
+ )
35
+
36
+ type TorrentSession struct {
37
+ Client *torrent.Client
38
+ Torrent *torrent.Torrent
39
+ Port int
40
+ LastUsed time.Time
41
+ }
42
+
43
+ type Settings struct {
44
+ EnableProxy bool `json:"enableProxy"`
45
+ ProxyURL string `json:"proxyUrl"`
46
+ EnableProwlarr bool `json:"enableProwlarr"`
47
+ ProwlarrHost string `json:"prowlarrHost"`
48
+ ProwlarrApiKey string `json:"prowlarrApiKey"`
49
+ EnableJackett bool `json:"enableJackett"`
50
+ JackettHost string `json:"jackettHost"`
51
+ JackettApiKey string `json:"jackettApiKey"`
52
+ }
53
+
54
+ type ProxySettings struct {
55
+ EnableProxy bool `json:"enableProxy"`
56
+ ProxyURL string `json:"proxyUrl"`
57
+ }
58
+
59
+ type ProwlarrSettings struct {
60
+ EnableProwlarr bool `json:"enableProwlarr"`
61
+ ProwlarrHost string `json:"prowlarrHost"`
62
+ ProwlarrApiKey string `json:"prowlarrApiKey"`
63
+ }
64
+
65
+ type JackettSettings struct {
66
+ EnableJackett bool `json:"enableJackett"`
67
+ JackettHost string `json:"jackettHost"`
68
+ JackettApiKey string `json:"jackettApiKey"`
69
+ }
70
+
71
+ var (
72
+ sessions sync.Map
73
+ usedPorts sync.Map
74
+ portMutex sync.Mutex
75
+ )
76
+
77
+ // Helper function to format file sizes
78
+ func formatSize(sizeInBytes float64) string {
79
+ if sizeInBytes < 1024 {
80
+ return fmt.Sprintf("%.0f B", sizeInBytes)
81
+ }
82
+
83
+ sizeInKB := sizeInBytes / 1024
84
+ if sizeInKB < 1024 {
85
+ return fmt.Sprintf("%.2f KB", sizeInKB)
86
+ }
87
+
88
+ sizeInMB := sizeInKB / 1024
89
+ if sizeInMB < 1024 {
90
+ return fmt.Sprintf("%.2f MB", sizeInMB)
91
+ }
92
+
93
+ sizeInGB := sizeInMB / 1024
94
+ return fmt.Sprintf("%.2f GB", sizeInGB)
95
+ }
96
+
97
+ var (
98
+ proxyTransport = &http.Transport{
99
+ // copy your existing timeouts & DialContext logic here...
100
+ TLSHandshakeTimeout: 10 * time.Second,
101
+ ResponseHeaderTimeout: 20 * time.Second,
102
+ ExpectContinueTimeout: 1 * time.Second,
103
+ IdleConnTimeout: 30 * time.Second,
104
+ MaxIdleConnsPerHost: 10,
105
+ }
106
+ proxyClient = &http.Client{
107
+ Transport: proxyTransport,
108
+ Timeout: 30 * time.Second,
109
+ CheckRedirect: func(req *http.Request, via []*http.Request) error {
110
+ if len(via) >= 10 {
111
+ return errors.New("too many redirects")
112
+ }
113
+ for k, vv := range via[0].Header {
114
+ if _, ok := req.Header[k]; !ok {
115
+ req.Header[k] = vv
116
+ }
117
+ }
118
+ return nil
119
+ },
120
+ }
121
+ )
122
+
123
+ func createSelectiveProxyClient() *http.Client {
124
+ settingsMutex.RLock()
125
+ defer settingsMutex.RUnlock()
126
+
127
+ if !currentSettings.EnableProxy {
128
+ return &http.Client{Timeout: 30 * time.Second}
129
+ }
130
+ // Reconfigure proxyTransport’s DialContext if URL changed:
131
+ dialer, _ := createProxyDialer(currentSettings.ProxyURL)
132
+ proxyTransport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
133
+ return dialer.Dial(network, addr)
134
+ }
135
+ // Drop any old idle conns after reconfiguration:
136
+ proxyTransport.CloseIdleConnections()
137
+
138
+ return proxyClient
139
+ }
140
+
141
+ // Create a proxy dialer for SOCKS5
142
+ func createProxyDialer(proxyURL string) (proxy.Dialer, error) {
143
+ proxyURLParsed, err := url.Parse(proxyURL)
144
+ if err != nil {
145
+ return nil, fmt.Errorf("failed to parse proxy URL: %v", err)
146
+ }
147
+
148
+ // Extract auth information
149
+ auth := &proxy.Auth{}
150
+ if proxyURLParsed.User != nil {
151
+ auth.User = proxyURLParsed.User.Username()
152
+ if password, ok := proxyURLParsed.User.Password(); ok {
153
+ auth.Password = password
154
+ }
155
+ }
156
+
157
+ // Create a SOCKS5 dialer
158
+ return proxy.SOCKS5("tcp", proxyURLParsed.Host, auth, proxy.Direct)
159
+ }
160
+
161
+ // Implement a port allocation function to prevent conflicts
162
+ func getAvailablePort() int {
163
+ portMutex.Lock()
164
+ defer portMutex.Unlock()
165
+
166
+ // Try up to 50 times to find an unused port
167
+ for i := 0; i < 50; i++ {
168
+ // Generate a random port in the high range
169
+ port := 10000 + rand.Intn(50000)
170
+
171
+ // Check if this port is already in use by our app
172
+ if _, exists := usedPorts.Load(port); !exists {
173
+ // Mark this port as used
174
+ usedPorts.Store(port, true)
175
+ return port
176
+ }
177
+ }
178
+
179
+ // If we can't find an available port, return a very high random port
180
+ // as a last resort
181
+ return 60000 + rand.Intn(5000)
182
+ }
183
+
184
+ // Release a port when we're done with it
185
+ func releasePort(port int) {
186
+ portMutex.Lock()
187
+ defer portMutex.Unlock()
188
+ usedPorts.Delete(port)
189
+ }
190
+
191
+ // Initialize the torrent client with proxy settings
192
+ func initTorrentWithProxy() (*torrent.Client, int, error) {
193
+ settingsMutex.RLock()
194
+ enableProxy := currentSettings.EnableProxy
195
+ proxyURL := currentSettings.ProxyURL
196
+ settingsMutex.RUnlock()
197
+
198
+ config := torrent.NewDefaultClientConfig()
199
+ config.DefaultStorage = storage.NewFile("./torrent-data")
200
+ port := getAvailablePort()
201
+ config.ListenPort = port
202
+
203
+ if enableProxy {
204
+ log.Println("Creating torrent client with proxy...")
205
+ os.Setenv("ALL_PROXY", proxyURL)
206
+ os.Setenv("SOCKS_PROXY", proxyURL)
207
+ os.Setenv("HTTP_PROXY", proxyURL)
208
+ os.Setenv("HTTPS_PROXY", proxyURL)
209
+
210
+ proxyDialer, err := createProxyDialer(proxyURL)
211
+ if err != nil {
212
+ releasePort(port)
213
+ return nil, port, fmt.Errorf("could not create proxy dialer: %v", err)
214
+ }
215
+
216
+ config.HTTPProxy = func(*http.Request) (*url.URL, error) {
217
+ return url.Parse(proxyURL)
218
+ }
219
+
220
+ client, err := torrent.NewClient(config)
221
+ if err != nil {
222
+ releasePort(port)
223
+ return nil, port, err
224
+ }
225
+
226
+ setValue(client, "dialerNetwork", func(ctx context.Context, network, addr string) (net.Conn, error) {
227
+ return proxyDialer.Dial(network, addr)
228
+ })
229
+
230
+ return client, port, nil
231
+ }
232
+
233
+ log.Println("Creating torrent client without proxy...")
234
+ os.Unsetenv("ALL_PROXY")
235
+ os.Unsetenv("SOCKS_PROXY")
236
+ os.Unsetenv("HTTP_PROXY")
237
+ os.Unsetenv("HTTPS_PROXY")
238
+
239
+ client, err := torrent.NewClient(config)
240
+ if err != nil {
241
+ releasePort(port)
242
+ return nil, port, err
243
+ }
244
+ return client, port, nil
245
+ }
246
+
247
+ // Helper function to try to set a field value using reflection
248
+ // This is a bit hacky but might help override the client's dialer
249
+ func setValue(obj interface{}, fieldName string, value interface{}) {
250
+ // This is a best-effort approach that may not work with all library versions
251
+ defer func() {
252
+ if r := recover(); r != nil {
253
+ log.Printf("Warning: Could not set %s field: %v", fieldName, r)
254
+ }
255
+ }()
256
+
257
+ reflectValue := reflect.ValueOf(obj).Elem()
258
+ field := reflectValue.FieldByName(fieldName)
259
+
260
+ if field.IsValid() && field.CanSet() {
261
+ field.Set(reflect.ValueOf(value))
262
+ log.Printf("Successfully set %s to use proxy", fieldName)
263
+ }
264
+ }
265
+
266
+ // Override system settings with our proxy
267
+ func init() {
268
+
269
+ // check if settings.json exists
270
+ if _, err := os.Stat("config/settings.json"); os.IsNotExist(err) {
271
+ log.Println("settings.json not found, creating default settings")
272
+ defaultSettings := Settings{
273
+ EnableProxy: false,
274
+ ProxyURL: "",
275
+ EnableProwlarr: false,
276
+ ProwlarrHost: "",
277
+ ProwlarrApiKey: "",
278
+ EnableJackett: false,
279
+ JackettHost: "",
280
+ JackettApiKey: "",
281
+ }
282
+ // Create the config directory if it doesn't exist
283
+ if err := os.MkdirAll("config", 0755); err != nil {
284
+ log.Fatalf("Failed to create config directory: %v", err)
285
+ }
286
+ settingsFile, err := os.Create("config/settings.json")
287
+ if err != nil {
288
+ log.Fatalf("Failed to create settings.json: %v", err)
289
+ }
290
+ defer settingsFile.Close()
291
+ encoder := json.NewEncoder(settingsFile)
292
+ encoder.SetIndent("", " ")
293
+ if err := encoder.Encode(defaultSettings); err != nil {
294
+ log.Fatalf("Failed to encode default settings: %v", err)
295
+ }
296
+ log.Println("Default settings created in settings.json")
297
+ }
298
+
299
+ // Load settings from settings.json
300
+ settingsFile, err := os.Open("config/settings.json")
301
+ if err != nil {
302
+ log.Fatalf("Failed to open settings.json: %v", err)
303
+ }
304
+ defer settingsFile.Close()
305
+
306
+ var s Settings
307
+ if err := json.NewDecoder(settingsFile).Decode(&s); err != nil {
308
+ log.Fatalf("Failed to decode settings.json: %v", err)
309
+ }
310
+
311
+ settingsMutex.Lock()
312
+ currentSettings = s
313
+ settingsMutex.Unlock()
314
+ }
315
+
316
+ func main() {
317
+ // Seed random number generator
318
+ rand.Seed(time.Now().UnixNano())
319
+
320
+ // Force proxy for all Go HTTP connections
321
+ setGlobalProxy()
322
+
323
+ // Set up endpoint handlers
324
+ http.HandleFunc("/api/v1/torrent/add", addTorrentHandler)
325
+ http.HandleFunc("/api/v1/torrent/", torrentHandler)
326
+ http.HandleFunc("/api/v1/settings", func(w http.ResponseWriter, r *http.Request) {
327
+ if r.Method == http.MethodGet {
328
+ settingsMutex.RLock()
329
+ defer settingsMutex.RUnlock()
330
+ respondWithJSON(w, http.StatusOK, currentSettings)
331
+ } else {
332
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
333
+ }
334
+ })
335
+ http.HandleFunc("/api/v1/settings/proxy", saveProxySettingsHandler)
336
+ http.HandleFunc("/api/v1/settings/prowlarr", saveProwlarrSettingsHandler)
337
+ http.HandleFunc("/api/v1/settings/jackett", saveJackettSettingsHandler)
338
+ http.HandleFunc("/api/v1/prowlarr/search", searchFromProwlarr)
339
+ http.HandleFunc("/api/v1/jackett/search", searchFromJackett)
340
+ http.HandleFunc("/api/v1/prowlarr/test", testProwlarrConnection)
341
+ http.HandleFunc("/api/v1/jackett/test", testJackettConnection)
342
+ http.HandleFunc("/api/v1/proxy/test", testProxyConnection)
343
+ http.HandleFunc("/api/v1/torrent/convert", convertTorrentToMagnetHandler)
344
+
345
+ // Set up client file serving
346
+ http.Handle("/", http.FileServer(http.Dir("./client")))
347
+ http.HandleFunc("/client/", func(w http.ResponseWriter, r *http.Request) {
348
+ http.StripPrefix("/client/", http.FileServer(http.Dir("./client"))).ServeHTTP(w, r)
349
+ })
350
+ http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
351
+ http.ServeFile(w, r, "./client/favicon.ico")
352
+ })
353
+
354
+ go cleanupSessions()
355
+
356
+ port := 3347
357
+
358
+ addr := fmt.Sprintf(":%d", port)
359
+ log.Printf("Attempting to start server on %s", addr)
360
+
361
+ // Create channel to signal if server started successfully
362
+ serverStarted := make(chan bool, 1)
363
+
364
+ // Create a server with graceful shutdown
365
+ server := &http.Server{
366
+ Addr: addr,
367
+ Handler: nil, // Use the default ServeMux
368
+ }
369
+
370
+ // Start the server in a goroutine
371
+ go func() {
372
+ err := server.ListenAndServe()
373
+ if err != nil && err != http.ErrServerClosed {
374
+ log.Printf("Server failed on %s: %v", addr, err)
375
+ serverStarted <- false
376
+ }
377
+ }()
378
+
379
+ // Give the server a moment to start or fail
380
+ select {
381
+ case success := <-serverStarted:
382
+ if !success {
383
+ log.Printf("Server failed to start on %s", addr)
384
+ return
385
+ }
386
+ case <-time.After(1 * time.Second):
387
+ // No immediate error, assume it started successfully
388
+ log.Printf("🚀 Server successfully started on %s", addr)
389
+
390
+ // Create a simple message to display in the browser
391
+ fmt.Printf("\n------------------------------------------------\n")
392
+ fmt.Printf("✅ Server started! Open in your browser:\n")
393
+ fmt.Printf(" http://localhost:%d\n", port)
394
+ fmt.Printf("------------------------------------------------\n\n")
395
+
396
+ // Block forever (the server is running in a goroutine)
397
+ select {}
398
+ }
399
+ }
400
+
401
+ // Set up global proxy for all Go HTTP calls
402
+ func setGlobalProxy() {
403
+ settingsMutex.RLock()
404
+ enableProxy := currentSettings.EnableProxy
405
+ proxyURL := currentSettings.ProxyURL
406
+ settingsMutex.RUnlock()
407
+
408
+ if !enableProxy {
409
+ log.Println("Proxy is disabled, not setting global HTTP proxy.")
410
+ return
411
+ }
412
+
413
+ proxyDialer, err := createProxyDialer(proxyURL)
414
+ if err != nil {
415
+ log.Printf("Warning: Could not create proxy dialer: %v", err)
416
+ return
417
+ }
418
+
419
+ httpTransport, ok := http.DefaultTransport.(*http.Transport)
420
+ if ok {
421
+ httpTransport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
422
+ return proxyDialer.Dial(network, addr)
423
+ }
424
+ log.Printf("Successfully configured SOCKS5 proxy for all HTTP traffic: %s", proxyURL)
425
+ } else {
426
+ log.Println("⚠️ Warning: Could not override HTTP transport")
427
+ }
428
+ }
429
+
430
+ // Handler to add a torrent using a magnet link
431
+ func addTorrentHandler(w http.ResponseWriter, r *http.Request) {
432
+ var request struct{ Magnet string }
433
+ if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
434
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request"})
435
+ return
436
+ }
437
+
438
+ magnet := request.Magnet
439
+ if magnet == "" {
440
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "No magnet link provided"})
441
+ }
442
+
443
+ // handle http links like Prowlarr or Jackett
444
+ if strings.HasPrefix(request.Magnet, "http") {
445
+ // Use the client that bypasses proxy for Prowlarr
446
+ httpClient := createSelectiveProxyClient()
447
+
448
+ httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
449
+ return http.ErrUseLastResponse
450
+ }
451
+
452
+ // Make the HTTP request to follow the Prowlarr link
453
+ req, err := http.NewRequest("GET", request.Magnet, nil)
454
+ if err != nil {
455
+ log.Printf("Error creating request: %v", err)
456
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{
457
+ "error": "Invalid URL: " + err.Error(),
458
+ })
459
+ return
460
+ }
461
+
462
+ req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
463
+
464
+ // Follow the Prowlarr link
465
+ log.Printf("Following Prowlarr URL: %s", request.Magnet)
466
+ resp, err := httpClient.Do(req)
467
+ if err != nil {
468
+ log.Printf("Error following URL: %v", err)
469
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{
470
+ "error": "Failed to download: " + err.Error(),
471
+ })
472
+ return
473
+ }
474
+ defer resp.Body.Close()
475
+
476
+ log.Printf("Got response: %d %s", resp.StatusCode, resp.Status)
477
+
478
+ // Check for redirects to magnet links
479
+ if resp.StatusCode >= 300 && resp.StatusCode < 400 {
480
+ location := resp.Header.Get("Location")
481
+ log.Printf("Found redirect to: %s", location)
482
+
483
+ if strings.HasPrefix(location, "magnet:") {
484
+ log.Printf("Found magnet redirect: %s", location)
485
+ magnet = location
486
+ } else {
487
+ log.Printf("Non-magnet redirect: %s", location)
488
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{
489
+ "error": "URL redirects to non-magnet content",
490
+ })
491
+ return
492
+ }
493
+ }
494
+ }
495
+
496
+ // check if magnet link is valid
497
+ if magnet == "" || !strings.HasPrefix(magnet, "magnet:") {
498
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid magnet link"})
499
+ return
500
+ }
501
+
502
+ // Use the simpler, more secure proxy configuration
503
+ client, port, err := initTorrentWithProxy()
504
+ if err != nil {
505
+ log.Printf("Client creation error: %v", err)
506
+ respondWithJSON(w, http.StatusInternalServerError,
507
+ map[string]string{"error": "Failed to create client with proxy"})
508
+ return
509
+ }
510
+
511
+ // if we bail out before session‑storage, make sure to release both client & port
512
+ defer func() {
513
+ if client != nil {
514
+ releasePort(port)
515
+ client.Close()
516
+ }
517
+ }()
518
+
519
+ t, err := client.AddMagnet(magnet)
520
+ if err != nil {
521
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid magnet url"})
522
+ return
523
+ }
524
+ log.Printf("Torrent added: %s", t.InfoHash().HexString())
525
+
526
+ select {
527
+ case <-t.GotInfo():
528
+ log.Printf("Successfully got torrent info for %s", t.InfoHash().HexString())
529
+ case <-time.After(3 * time.Minute):
530
+ respondWithJSON(w, http.StatusGatewayTimeout, map[string]string{"error": "Timeout getting info - proxy might be blocking BitTorrent traffic"})
531
+ }
532
+
533
+ sessionID := t.InfoHash().HexString()
534
+ log.Printf("Creating new session with ID: %s", sessionID)
535
+ sessions.Store(sessionID, &TorrentSession{
536
+ Client: client,
537
+ Torrent: t,
538
+ Port: port,
539
+ LastUsed: time.Now(),
540
+ })
541
+
542
+ // Log successful storage
543
+ log.Printf("Successfully stored session: %s", sessionID)
544
+
545
+ // Set client to nil so it doesn't get closed by the defer function
546
+ // since it's now stored in the sessions map
547
+ client = nil
548
+
549
+ respondWithJSON(w, http.StatusOK, map[string]string{"sessionId": sessionID})
550
+ }
551
+
552
+ // Torrent handler to serve torrent files and stream content
553
+ func torrentHandler(w http.ResponseWriter, r *http.Request) {
554
+ // Log the entire URL path for debugging
555
+ log.Printf("Torrent handler called with path: %s", r.URL.Path)
556
+
557
+ // Extract sessionId and possibly fileIndex from the URL
558
+ parts := strings.Split(r.URL.Path, "/")
559
+
560
+ // Debug the path parts
561
+ log.Printf("Path parts: %v (length: %d)", parts, len(parts))
562
+
563
+ // The URL structure is /api/v1/torrent/[sessionId]/...
564
+ if len(parts) < 5 { // Changed from 4 to 5
565
+ log.Printf("Invalid path: not enough parts")
566
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid path"})
567
+ return
568
+ }
569
+
570
+ // The session ID is at position 4, not 3 (because array is 0-indexed and path starts with /)
571
+ sessionID := parts[4] // Changed from parts[3] to parts[4]
572
+
573
+ log.Printf("Looking for session with ID: %s", sessionID)
574
+
575
+ // Debug: Print all sessions that we have
576
+ var sessionKeys []string
577
+ sessions.Range(func(key, value interface{}) bool {
578
+ keyStr, ok := key.(string)
579
+ if ok {
580
+ sessionKeys = append(sessionKeys, keyStr)
581
+ }
582
+ return true
583
+ })
584
+ log.Printf("Available sessions: %v", sessionKeys)
585
+
586
+ // Get the torrent session from our sessions map
587
+ sessionValue, ok := sessions.Load(sessionID)
588
+ if !ok {
589
+ log.Printf("Session not found with ID: %s", sessionID)
590
+ respondWithJSON(w, http.StatusNotFound, map[string]string{
591
+ "error": "Session not found",
592
+ "id": sessionID,
593
+ "available_sessions": strings.Join(sessionKeys, ", "),
594
+ })
595
+ return
596
+ }
597
+
598
+ log.Printf("Found session with ID: %s", sessionID)
599
+ session := sessionValue.(*TorrentSession)
600
+ session.LastUsed = time.Now() // Update last used time
601
+
602
+ // If there's a streaming request, handle it
603
+ if len(parts) > 5 && parts[5] == "stream" { // Changed from parts[4] to parts[5]
604
+ if len(parts) < 7 { // Changed from 6 to 7
605
+ http.Error(w, "Invalid stream path", http.StatusBadRequest)
606
+ return
607
+ }
608
+
609
+ fileIndexString := parts[6]
610
+ // remove .vtt from fileIndex if it exists
611
+ fileIndexString = strings.TrimSuffix(fileIndexString, ".vtt")
612
+
613
+ fileIndex, err := strconv.Atoi(fileIndexString)
614
+
615
+ if err != nil {
616
+ http.Error(w, "Invalid file index", http.StatusBadRequest)
617
+ return
618
+ }
619
+
620
+ if fileIndex < 0 || fileIndex >= len(session.Torrent.Files()) {
621
+ http.Error(w, "File index out of range", http.StatusBadRequest)
622
+ return
623
+ }
624
+
625
+ file := session.Torrent.Files()[fileIndex]
626
+
627
+ // Set appropriate Content-Type based on file extension
628
+ fileName := file.DisplayPath()
629
+ extension := strings.ToLower(filepath.Ext(fileName))
630
+
631
+ log.Printf("Streaming file: %s (type: %s)", fileName, extension)
632
+
633
+ switch extension {
634
+ case ".mp4":
635
+ w.Header().Set("Content-Type", "video/mp4")
636
+ case ".webm":
637
+ w.Header().Set("Content-Type", "video/webm")
638
+ case ".mkv":
639
+ w.Header().Set("Content-Type", "video/x-matroska")
640
+ case ".avi":
641
+ w.Header().Set("Content-Type", "video/x-msvideo")
642
+ case ".srt":
643
+ // For SRT, convert to VTT on-the-fly if requested as VTT
644
+ if r.URL.Query().Get("format") == "vtt" {
645
+ w.Header().Set("Content-Type", "text/vtt")
646
+ w.Header().Set("Access-Control-Allow-Origin", "*") // Allow cross-origin requests
647
+
648
+ // Read the SRT file with size limit
649
+ reader := file.NewReader()
650
+ // Wrap with limiting reader to prevent memory issues (10MB max)
651
+ limitReader := io.LimitReader(reader, 10*1024*1024) // 10MB limit for subtitles
652
+ srtBytes, err := io.ReadAll(limitReader)
653
+ if err != nil {
654
+ http.Error(w, "Failed to read subtitle file", http.StatusInternalServerError)
655
+ return
656
+ }
657
+
658
+ // Convert from SRT to VTT
659
+ vttBytes := convertSRTtoVTT(srtBytes)
660
+ w.Write(vttBytes)
661
+ return
662
+ } else {
663
+ w.Header().Set("Content-Type", "text/plain")
664
+ w.Header().Set("Access-Control-Allow-Origin", "*") // Allow cross-origin requests
665
+ }
666
+ case ".vtt":
667
+ w.Header().Set("Content-Type", "text/vtt")
668
+ w.Header().Set("Access-Control-Allow-Origin", "*") // Allow cross-origin requests
669
+ case ".sub":
670
+ w.Header().Set("Content-Type", "text/plain")
671
+ w.Header().Set("Access-Control-Allow-Origin", "*") // Allow cross-origin requests
672
+ default:
673
+ w.Header().Set("Content-Type", "application/octet-stream")
674
+ }
675
+
676
+ // Add CORS headers for all content
677
+ // Stream the file
678
+ reader := file.NewReader()
679
+ // ServeContent will close the reader when done but we need to
680
+ // ensure it gets closed if there's a panic or other error
681
+ defer func() {
682
+ if closer, ok := reader.(io.Closer); ok {
683
+ closer.Close()
684
+ println("Closed reader***************************************")
685
+ }
686
+ }()
687
+ println("Serving content*****************************************")
688
+ http.ServeContent(w, r, fileName, time.Time{}, reader)
689
+ return
690
+ }
691
+
692
+ // If we get here, just return file list
693
+ var files []map[string]interface{}
694
+ for i, file := range session.Torrent.Files() {
695
+ files = append(files, map[string]interface{}{
696
+ "index": i,
697
+ "name": file.DisplayPath(),
698
+ "size": file.Length(),
699
+ })
700
+ }
701
+
702
+ respondWithJSON(w, http.StatusOK, files)
703
+ }
704
+
705
+ // Add a function to convert SRT to VTT format
706
+ func convertSRTtoVTT(srtBytes []byte) []byte {
707
+ srtContent := string(srtBytes)
708
+
709
+ // Add VTT header
710
+ vttContent := "WEBVTT\n\n"
711
+
712
+ // Convert SRT content to VTT format
713
+ // Simple conversion - replace timestamps format
714
+ lines := strings.Split(srtContent, "\n")
715
+
716
+ for i := 0; i < len(lines); i++ {
717
+ line := lines[i]
718
+
719
+ // Skip subtitle numbers
720
+ if _, err := strconv.Atoi(strings.TrimSpace(line)); err == nil {
721
+ continue
722
+ }
723
+
724
+ // Convert timestamp lines
725
+ if strings.Contains(line, " --> ") {
726
+ // SRT: 00:00:20,000 --> 00:00:24,400
727
+ // VTT: 00:00:20.000 --> 00:00:24.400
728
+ line = strings.Replace(line, ",", ".", -1)
729
+ vttContent += line + "\n"
730
+ } else {
731
+ vttContent += line + "\n"
732
+ }
733
+ }
734
+
735
+ return []byte(vttContent)
736
+ }
737
+
738
+ // Helper function to respond with JSON
739
+ func respondWithJSON(w http.ResponseWriter, status int, data interface{}) {
740
+ w.Header().Set("Content-Type", "application/json")
741
+ w.WriteHeader(status)
742
+ json.NewEncoder(w).Encode(data)
743
+ }
744
+
745
+ // Update cleanupSessions with safer reflection
746
+ func cleanupSessions() {
747
+ ticker := time.NewTicker(5 * time.Minute)
748
+ defer ticker.Stop()
749
+
750
+ for range ticker.C {
751
+ log.Printf("Checking for unused sessions...")
752
+ sessions.Range(func(key, value interface{}) bool {
753
+ session := value.(*TorrentSession)
754
+
755
+ if time.Since(session.LastUsed) > 15*time.Minute {
756
+ releasePort(session.Port)
757
+ session.Torrent.Drop()
758
+ session.Client.Close()
759
+ sessions.Delete(key)
760
+ log.Printf("Removed unused session: %s", key)
761
+ }
762
+ return true
763
+ })
764
+ runtime.GC()
765
+ }
766
+ }
767
+
768
+ // Test the proxy connection
769
+ func testProwlarrConnection(w http.ResponseWriter, r *http.Request) {
770
+ // Add CORS headers
771
+ w.Header().Set("Access-Control-Allow-Origin", "*")
772
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
773
+
774
+ // Handle preflight requests
775
+ if r.Method == "OPTIONS" {
776
+ return
777
+ }
778
+
779
+ var settings ProwlarrSettings
780
+ if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
781
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
782
+ return
783
+ }
784
+
785
+ prowlarrHost := settings.ProwlarrHost
786
+ prowlarrApiKey := settings.ProwlarrApiKey
787
+
788
+ if prowlarrHost == "" || prowlarrApiKey == "" {
789
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Prowlarr host or API key not set"})
790
+ return
791
+ }
792
+
793
+ client := createSelectiveProxyClient()
794
+ testURL := fmt.Sprintf("%s/api/v1/system/status", prowlarrHost)
795
+
796
+ req, err := http.NewRequest("GET", testURL, nil)
797
+ if err != nil {
798
+ log.Printf("Error creating request: %v", err)
799
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
800
+ return
801
+ }
802
+
803
+ req.Header.Set("X-Api-Key", prowlarrApiKey)
804
+ resp, err := client.Do(req)
805
+ if err != nil {
806
+ log.Printf("Error making request to Prowlarr: %v", err)
807
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to connect to Prowlarr: " + err.Error()})
808
+ return
809
+ }
810
+ defer resp.Body.Close()
811
+
812
+ if resp.StatusCode != http.StatusOK {
813
+ respondWithJSON(w, resp.StatusCode, map[string]string{"error": fmt.Sprintf("Prowlarr returned status %d", resp.StatusCode)})
814
+ return
815
+ }
816
+
817
+ responseBody, err := io.ReadAll(resp.Body)
818
+ if err != nil {
819
+ log.Printf("Error reading response: %v", err)
820
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to read Prowlarr response"})
821
+ return
822
+ }
823
+
824
+ w.Header().Set("Content-Type", "application/json")
825
+ w.WriteHeader(http.StatusOK)
826
+ w.Write(responseBody)
827
+ }
828
+
829
+ // Search from Prowlarr
830
+ func searchFromProwlarr(w http.ResponseWriter, r *http.Request) {
831
+ // Add CORS headers
832
+ w.Header().Set("Access-Control-Allow-Origin", "*")
833
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Prowlarr-Host, X-Api-Key")
834
+
835
+ // Handle preflight requests
836
+ if r.Method == "OPTIONS" {
837
+ return
838
+ }
839
+
840
+ if r.Method != http.MethodPost {
841
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
842
+ return
843
+ }
844
+
845
+ query := r.URL.Query().Get("q")
846
+ if query == "" {
847
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "No search query provided"})
848
+ return
849
+ }
850
+
851
+ // search movies in prowlarr
852
+ settingsMutex.RLock()
853
+ prowlarrHost := currentSettings.ProwlarrHost
854
+ prowlarrApiKey := currentSettings.ProwlarrApiKey
855
+ settingsMutex.RUnlock()
856
+
857
+ if prowlarrHost == "" || prowlarrApiKey == "" {
858
+ http.Error(w, "Prowlarr host or API key not set", http.StatusBadRequest)
859
+ return
860
+ }
861
+
862
+ // Use the client that bypasses proxy for Prowlarr
863
+ client := createSelectiveProxyClient()
864
+
865
+ // Prowlarr search endpoint - looking for movie torrents
866
+ searchURL := fmt.Sprintf("%s/api/v1/search?query=%s&limit=10", prowlarrHost, url.QueryEscape(query))
867
+
868
+ req, err := http.NewRequest("GET", searchURL, nil)
869
+ if err != nil {
870
+ log.Printf("Error creating request: %v", err)
871
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
872
+ return
873
+ }
874
+
875
+ req.Header.Set("X-Api-Key", prowlarrApiKey)
876
+ resp, err := client.Do(req)
877
+ if err != nil {
878
+ log.Printf("Error making request to Prowlarr: %v", err)
879
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to connect to Prowlarr: " + err.Error()})
880
+ return
881
+ }
882
+ defer resp.Body.Close()
883
+
884
+ // Read the response body
885
+ body, err := io.ReadAll(resp.Body)
886
+ if err != nil {
887
+ log.Printf("Error reading response: %v", err)
888
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to read Prowlarr response"})
889
+ return
890
+ }
891
+
892
+ if resp.StatusCode != http.StatusOK {
893
+ respondWithJSON(w, resp.StatusCode, map[string]string{"error": fmt.Sprintf("Prowlarr returned status %d: %s", resp.StatusCode, string(body))})
894
+ return
895
+ }
896
+
897
+ // Parse the JSON response and process the results
898
+ var results []map[string]interface{}
899
+ if err := json.Unmarshal(body, &results); err != nil {
900
+ log.Printf("Error parsing JSON: %v", err)
901
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to parse Prowlarr response"})
902
+ return
903
+ }
904
+
905
+ // Process the results to make them more usable by the frontend
906
+ var processedResults []map[string]interface{}
907
+ for _, result := range results {
908
+ // Get title and download URL
909
+ title, hasTitle := result["title"].(string)
910
+ downloadUrl, hasDownloadUrl := result["downloadUrl"].(string)
911
+
912
+ // Magnet URL might be present in some results
913
+ magnetUrl, hasMagnet := result["magnetUrl"].(string)
914
+
915
+ if !hasTitle || title == "" {
916
+ // Skip results without titles
917
+ continue
918
+ }
919
+
920
+ // We need at least one of download URL or magnet URL
921
+ if (!hasDownloadUrl || downloadUrl == "") && (!hasMagnet || magnetUrl == "") {
922
+ continue
923
+ }
924
+
925
+ // Create a simplified result object with just what we need
926
+ processedResult := map[string]interface{}{
927
+ "title": title,
928
+ }
929
+
930
+ // Prefer magnet URLs if available directly
931
+ if hasMagnet && magnetUrl != "" {
932
+ processedResult["magnetUrl"] = magnetUrl
933
+ processedResult["directMagnet"] = true
934
+ } else if hasDownloadUrl && downloadUrl != "" {
935
+ processedResult["downloadUrl"] = downloadUrl
936
+ processedResult["directMagnet"] = false
937
+ }
938
+
939
+ // Include optional fields if they exist
940
+ if size, ok := result["size"].(float64); ok {
941
+ processedResult["size"] = formatSize(size)
942
+ }
943
+
944
+ if seeders, ok := result["seeders"].(float64); ok {
945
+ processedResult["seeders"] = seeders
946
+ }
947
+
948
+ if leechers, ok := result["leechers"].(float64); ok {
949
+ processedResult["leechers"] = leechers
950
+ }
951
+
952
+ if indexer, ok := result["indexer"].(string); ok {
953
+ processedResult["indexer"] = indexer
954
+ }
955
+
956
+ if publishDate, ok := result["publishDate"].(string); ok {
957
+ processedResult["publishDate"] = publishDate
958
+ }
959
+
960
+ if category, ok := result["category"].(string); ok {
961
+ processedResult["category"] = category
962
+ }
963
+
964
+ processedResults = append(processedResults, processedResult)
965
+ }
966
+
967
+ respondWithJSON(w, http.StatusOK, processedResults)
968
+ }
969
+
970
+ // Test Jackett Connection Handler
971
+ func testJackettConnection(w http.ResponseWriter, r *http.Request) {
972
+ // Add CORS headers
973
+ w.Header().Set("Access-Control-Allow-Origin", "*")
974
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
975
+ // Handle preflight requests
976
+ if r.Method == "OPTIONS" {
977
+ return
978
+ }
979
+
980
+ var settings JackettSettings
981
+ if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
982
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
983
+ return
984
+ }
985
+
986
+ jackettHost := settings.JackettHost
987
+ jackettApiKey := settings.JackettApiKey
988
+
989
+ if jackettHost == "" || jackettApiKey == "" {
990
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Jackett host or API key not set"})
991
+ return
992
+ }
993
+
994
+ client := createSelectiveProxyClient()
995
+ testURL := fmt.Sprintf("%s/api/v2.0/indexers/all/results?apikey=%s", jackettHost, jackettApiKey)
996
+ req, err := http.NewRequest("GET", testURL, nil)
997
+ if err != nil {
998
+ log.Printf("Error creating request: %v", err)
999
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
1000
+ return
1001
+ }
1002
+ resp, err := client.Do(req)
1003
+ if err != nil {
1004
+ log.Printf("Error making request to Jackett: %v", err)
1005
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to connect to Jackett: " + err.Error()})
1006
+ return
1007
+ }
1008
+ defer resp.Body.Close()
1009
+ if resp.StatusCode != http.StatusOK {
1010
+ respondWithJSON(w, resp.StatusCode, map[string]string{"error": fmt.Sprintf("Jackett returned status %d", resp.StatusCode)})
1011
+ return
1012
+ }
1013
+ responseBody, err := io.ReadAll(resp.Body)
1014
+ if err != nil {
1015
+ log.Printf("Error reading response: %v", err)
1016
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to read Jackett response"})
1017
+ return
1018
+ }
1019
+ w.Header().Set("Content-Type", "application/json")
1020
+ w.WriteHeader(http.StatusOK)
1021
+ w.Write(responseBody)
1022
+ }
1023
+
1024
+ // Search from Jackett
1025
+ func searchFromJackett(w http.ResponseWriter, r *http.Request) {
1026
+ // Add CORS headers
1027
+ w.Header().Set("Access-Control-Allow-Origin", "*")
1028
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
1029
+
1030
+ // Handle preflight requests
1031
+ if r.Method == "OPTIONS" {
1032
+ return
1033
+ }
1034
+
1035
+ if r.Method != http.MethodPost {
1036
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
1037
+ return
1038
+ }
1039
+
1040
+ query := r.URL.Query().Get("q")
1041
+ if query == "" {
1042
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "No search query provided"})
1043
+ return
1044
+ }
1045
+
1046
+ // search movies in jackett
1047
+ settingsMutex.RLock()
1048
+ jackettHost := currentSettings.JackettHost
1049
+ jackettApiKey := currentSettings.JackettApiKey
1050
+ settingsMutex.RUnlock()
1051
+
1052
+ if jackettHost == "" || jackettApiKey == "" {
1053
+ http.Error(w, "Jackett host or API key not set", http.StatusBadRequest)
1054
+ return
1055
+ }
1056
+
1057
+ // Use the client that bypasses proxy for Jackett
1058
+ client := createSelectiveProxyClient()
1059
+
1060
+ // Jackett search endpoint - looking for movie torrents
1061
+ searchURL := fmt.Sprintf("%s/api/v2.0/indexers/all/results?Query=%s&apikey=%s", jackettHost, url.QueryEscape(query), jackettApiKey)
1062
+
1063
+ req, err := http.NewRequest("GET", searchURL, nil)
1064
+ if err != nil {
1065
+ log.Printf("Error creating request: %v", err)
1066
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
1067
+ return
1068
+ }
1069
+
1070
+ resp, err := client.Do(req)
1071
+ if err != nil {
1072
+ log.Printf("Error making request to Jackett: %v", err)
1073
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to connect to Jackett: " + err.Error()})
1074
+ return
1075
+ }
1076
+ defer resp.Body.Close()
1077
+
1078
+ // Read the response body
1079
+ body, err := io.ReadAll(resp.Body)
1080
+ if err != nil {
1081
+ log.Printf("Error reading response: %v", err)
1082
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to read Jackett response"})
1083
+ return
1084
+ }
1085
+
1086
+ if resp.StatusCode != http.StatusOK {
1087
+ respondWithJSON(w, resp.StatusCode, map[string]string{"error": fmt.Sprintf("Jackett returned status %d: %s", resp.StatusCode, string(body))})
1088
+ return
1089
+ }
1090
+
1091
+ var jacketResponse struct {
1092
+ Results []map[string]interface{} `json:"Results"`
1093
+ }
1094
+
1095
+ // Parse the JSON response and process the results
1096
+ if err := json.Unmarshal(body, &jacketResponse); err != nil {
1097
+ log.Printf("Error parsing JSON: %v", err)
1098
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to parse Jackett response"})
1099
+ return
1100
+ }
1101
+
1102
+ // Process the results to make them more usable by the frontend
1103
+ var processedResults []map[string]interface{}
1104
+ for _, result := range jacketResponse.Results {
1105
+ // Get title and download URL
1106
+ title, hasTitle := result["Title"].(string)
1107
+ downloadUrl, hasDownloadUrl := result["Link"].(string)
1108
+
1109
+ // Magnet URL might be present in some results
1110
+ magnetUrl, hasMagnet := result["MagnetUri"].(string)
1111
+
1112
+ if !hasTitle || title == "" {
1113
+ // Skip results without titles
1114
+ continue
1115
+ }
1116
+
1117
+ // We need at least one of download URL or magnet URL
1118
+ if (!hasDownloadUrl || downloadUrl == "") && (!hasMagnet || magnetUrl == "") {
1119
+ continue
1120
+ }
1121
+
1122
+ // Create a simplified result object with just what we need
1123
+ processedResult := map[string]interface{}{
1124
+ "title": title,
1125
+ }
1126
+
1127
+ // Prefer magnet URLs if available directly
1128
+ if hasMagnet && magnetUrl != "" && strings.HasPrefix(magnetUrl, "magnet:") {
1129
+ processedResult["magnetUrl"] = magnetUrl
1130
+ processedResult["directMagnet"] = true
1131
+ } else if hasDownloadUrl && downloadUrl != "" {
1132
+ processedResult["downloadUrl"] = downloadUrl
1133
+ processedResult["directMagnet"] = false
1134
+ }
1135
+
1136
+ // Include optional fields if they exist
1137
+ if size, ok := result["Size"].(float64); ok {
1138
+ processedResult["size"] = formatSize(size)
1139
+ }
1140
+
1141
+ if seeders, ok := result["Seeders"].(float64); ok {
1142
+ processedResult["seeders"] = seeders
1143
+ }
1144
+
1145
+ if leechers, ok := result["Peers"].(float64); ok {
1146
+ processedResult["leechers"] = leechers
1147
+ }
1148
+
1149
+ if indexer, ok := result["Tracker"].(string); ok {
1150
+ processedResult["indexer"] = indexer
1151
+ }
1152
+
1153
+ if publishDate, ok := result["PublishDate"].(string); ok {
1154
+ processedResult["publishDate"] = publishDate
1155
+ }
1156
+
1157
+ if category, ok := result["category"].(string); ok {
1158
+ processedResult["category"] = category
1159
+ }
1160
+
1161
+ processedResults = append(processedResults, processedResult)
1162
+ }
1163
+
1164
+ respondWithJSON(w, http.StatusOK, processedResults)
1165
+ }
1166
+
1167
+ // Test Proxy Connection Handler
1168
+ func testProxyConnection(w http.ResponseWriter, r *http.Request) {
1169
+ // Add CORS headers
1170
+ w.Header().Set("Access-Control-Allow-Origin", "*")
1171
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
1172
+
1173
+ // Handle preflight requests
1174
+ if r.Method == "OPTIONS" {
1175
+ return
1176
+ }
1177
+
1178
+ if r.Method != http.MethodPost {
1179
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
1180
+ return
1181
+ }
1182
+
1183
+ var settings ProxySettings
1184
+ if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
1185
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
1186
+ return
1187
+ }
1188
+
1189
+ proxyURL := settings.ProxyURL
1190
+
1191
+ if proxyURL == "" {
1192
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Proxy URL not set"})
1193
+ return
1194
+ }
1195
+
1196
+ // Parse the proxy URL
1197
+ parsedProxyURL, err := url.Parse(proxyURL)
1198
+ if err != nil {
1199
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid proxy URL: " + err.Error()})
1200
+ return
1201
+ }
1202
+
1203
+ // Create a transport that uses the proxy
1204
+ transport := &http.Transport{
1205
+ Proxy: http.ProxyURL(parsedProxyURL),
1206
+ }
1207
+
1208
+ // Create client with custom transport and timeout
1209
+ client := &http.Client{
1210
+ Transport: transport,
1211
+ Timeout: 10 * time.Second, // Adjust timeout as needed
1212
+ }
1213
+
1214
+ testURL := "https://httpbin.org/ip"
1215
+ req, err := http.NewRequest("GET", testURL, nil)
1216
+ if err != nil {
1217
+ log.Printf("Error creating request: %v", err)
1218
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
1219
+ return
1220
+ }
1221
+
1222
+ resp, err := client.Do(req)
1223
+ if err != nil {
1224
+ log.Printf("Error making request through proxy: %v", err)
1225
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Proxy connection failed: " + err.Error()})
1226
+ return
1227
+ }
1228
+ defer resp.Body.Close()
1229
+
1230
+ responseBody, err := io.ReadAll(resp.Body)
1231
+ if err != nil {
1232
+ log.Printf("Error reading response: %v", err)
1233
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to read proxy response"})
1234
+ return
1235
+ }
1236
+
1237
+ w.Header().Set("Content-Type", "application/json")
1238
+ w.WriteHeader(http.StatusOK)
1239
+ w.Write(responseBody)
1240
+ }
1241
+
1242
+ // Helper function to save settings to file (assumes mutex is already locked)
1243
+ func saveSettingsToFile() error {
1244
+ // Create the directory if it doesn't exist
1245
+ if err := os.MkdirAll("config", 0755); err != nil {
1246
+ log.Fatalf("Failed to create config directory: %v", err)
1247
+ }
1248
+
1249
+ file, err := os.Create("config/settings.json")
1250
+ if err != nil {
1251
+ return err
1252
+ }
1253
+ defer file.Close()
1254
+
1255
+ encoder := json.NewEncoder(file)
1256
+ encoder.SetIndent("", " ")
1257
+ if err := encoder.Encode(currentSettings); err != nil {
1258
+ return err
1259
+ }
1260
+
1261
+ return nil
1262
+ }
1263
+
1264
+ // Proxy Settings Save Handler
1265
+ func saveProxySettingsHandler(w http.ResponseWriter, r *http.Request) {
1266
+ w.Header().Set("Access-Control-Allow-Origin", "*")
1267
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
1268
+ if r.Method == "OPTIONS" {
1269
+ return
1270
+ }
1271
+ if r.Method != http.MethodPost {
1272
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
1273
+ return
1274
+ }
1275
+
1276
+ var newSettings ProxySettings
1277
+ if err := json.NewDecoder(r.Body).Decode(&newSettings); err != nil {
1278
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
1279
+ return
1280
+ }
1281
+
1282
+ settingsMutex.RLock()
1283
+ currentSettings.EnableProxy = newSettings.EnableProxy
1284
+ currentSettings.ProxyURL = newSettings.ProxyURL
1285
+ defer settingsMutex.RUnlock()
1286
+
1287
+ if err := saveSettingsToFile(); err != nil {
1288
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to save settings: " + err.Error()})
1289
+ return
1290
+ }
1291
+ println("Proxy settings saved successfully")
1292
+
1293
+ setGlobalProxy()
1294
+
1295
+ respondWithJSON(w, http.StatusOK, map[string]string{"message": "Proxy settings saved successfully"})
1296
+ }
1297
+
1298
+ // Prowlarr Settings Save Handler
1299
+ func saveProwlarrSettingsHandler(w http.ResponseWriter, r *http.Request) {
1300
+ w.Header().Set("Access-Control-Allow-Origin", "*")
1301
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
1302
+ if r.Method == "OPTIONS" {
1303
+ return
1304
+ }
1305
+ if r.Method != http.MethodPost {
1306
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
1307
+ return
1308
+ }
1309
+
1310
+ var newSettings ProwlarrSettings
1311
+ if err := json.NewDecoder(r.Body).Decode(&newSettings); err != nil {
1312
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
1313
+ return
1314
+ }
1315
+
1316
+ settingsMutex.RLock()
1317
+ currentSettings.EnableProwlarr = newSettings.EnableProwlarr
1318
+ currentSettings.ProwlarrHost = newSettings.ProwlarrHost
1319
+ currentSettings.ProwlarrApiKey = newSettings.ProwlarrApiKey
1320
+ defer settingsMutex.RUnlock()
1321
+
1322
+ if err := saveSettingsToFile(); err != nil {
1323
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to save settings: " + err.Error()})
1324
+ return
1325
+ }
1326
+
1327
+ respondWithJSON(w, http.StatusOK, map[string]string{"message": "Prowlarr settings saved successfully"})
1328
+ }
1329
+
1330
+ // Jackett Settings Save Handler
1331
+ func saveJackettSettingsHandler(w http.ResponseWriter, r *http.Request) {
1332
+ w.Header().Set("Access-Control-Allow-Origin", "*")
1333
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
1334
+ if r.Method == "OPTIONS" {
1335
+ return
1336
+ }
1337
+ if r.Method != http.MethodPost {
1338
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
1339
+ return
1340
+ }
1341
+
1342
+ var newSettings JackettSettings
1343
+ if err := json.NewDecoder(r.Body).Decode(&newSettings); err != nil {
1344
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
1345
+ return
1346
+ }
1347
+
1348
+ settingsMutex.RLock()
1349
+ currentSettings.EnableJackett = newSettings.EnableJackett
1350
+ currentSettings.JackettHost = newSettings.JackettHost
1351
+ currentSettings.JackettApiKey = newSettings.JackettApiKey
1352
+ defer settingsMutex.RUnlock()
1353
+
1354
+ if err := saveSettingsToFile(); err != nil {
1355
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to save settings: " + err.Error()})
1356
+ return
1357
+ }
1358
+
1359
+ respondWithJSON(w, http.StatusOK, map[string]string{"message": "Jackett settings saved successfully"})
1360
+ }
1361
+
1362
+ // Convert Torrent to Magnet Handler
1363
+ func convertTorrentToMagnetHandler(w http.ResponseWriter, r *http.Request) {
1364
+ // Set CORS headers
1365
+ w.Header().Set("Access-Control-Allow-Origin", "*")
1366
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
1367
+
1368
+ if r.Method == "OPTIONS" {
1369
+ return
1370
+ }
1371
+
1372
+ if r.Method != http.MethodPost {
1373
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
1374
+ return
1375
+ }
1376
+
1377
+ // Parse multipart form with 10MB memory limit
1378
+ const maxUploadSize = 10 << 20 // 10MB
1379
+ if err := r.ParseMultipartForm(maxUploadSize); err != nil {
1380
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Failed to parse form: " + err.Error()})
1381
+ return
1382
+ }
1383
+
1384
+ // Get the torrent file from the form data
1385
+ file, header, err := r.FormFile("torrent")
1386
+ if err != nil {
1387
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing torrent file"})
1388
+ return
1389
+ }
1390
+ defer file.Close()
1391
+
1392
+ // Check file size
1393
+ if header.Size > maxUploadSize {
1394
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "File too large"})
1395
+ return
1396
+ }
1397
+
1398
+ // Read the torrent file content
1399
+ fileBytes, err := io.ReadAll(file)
1400
+ if err != nil {
1401
+ respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to read file"})
1402
+ return
1403
+ }
1404
+
1405
+ // Parse torrent file
1406
+ mi, err := metainfo.Load(bytes.NewReader(fileBytes))
1407
+ if err != nil {
1408
+ respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid torrent file: " + err.Error()})
1409
+ return
1410
+ }
1411
+
1412
+ // Get info hash
1413
+ infoHash := mi.HashInfoBytes().String()
1414
+
1415
+ // Build magnet URL components
1416
+ magnet := fmt.Sprintf("magnet:?xt=urn:btih:%s", infoHash)
1417
+
1418
+ // Add display name
1419
+ info, err := mi.UnmarshalInfo()
1420
+ if err == nil {
1421
+ magnet += fmt.Sprintf("&dn=%s", url.QueryEscape(info.Name))
1422
+ }
1423
+
1424
+ // Add trackers
1425
+ for _, tier := range mi.AnnounceList {
1426
+ for _, tracker := range tier {
1427
+ magnet += fmt.Sprintf("&tr=%s", url.QueryEscape(tracker))
1428
+ }
1429
+ }
1430
+
1431
+ respondWithJSON(w, http.StatusOK, map[string]string{
1432
+ "magnet": magnet,
1433
+ })
1434
+ }