mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat(releases): show details in list view (#1337)
* feat(releases): show details in list view * fix(releases): activitytable columns type * fix(releases): incognito mode * feat(releases): move details button * do we wanna truncate? * fix(web): release column width at full size --------- Co-authored-by: martylukyy <35452459+martylukyy@users.noreply.github.com>
This commit is contained in:
parent
7eaf499d66
commit
9992675971
8 changed files with 155 additions and 91 deletions
|
@ -211,7 +211,7 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
|
||||||
}
|
}
|
||||||
|
|
||||||
queryBuilder := repo.db.squirrel.
|
queryBuilder := repo.db.squirrel.
|
||||||
Select("r.id", "r.filter_status", "r.rejections", "r.indexer", "r.filter", "r.protocol", "r.info_url", "r.download_url", "r.title", "r.torrent_name", "r.size", "r.timestamp",
|
Select("r.id", "r.filter_status", "r.rejections", "r.indexer", "r.filter", "r.protocol", "r.info_url", "r.download_url", "r.title", "r.torrent_name", "r.size", "r.category", "r.season", "r.episode", "r.year", "r.resolution", "r.source", "r.codec", "r.container", "r.release_group", "r.timestamp",
|
||||||
"ras.id", "ras.status", "ras.action", "ras.action_id", "ras.type", "ras.client", "ras.filter", "ras.filter_id", "ras.release_id", "ras.rejections", "ras.timestamp").
|
"ras.id", "ras.status", "ras.action", "ras.action_id", "ras.type", "ras.client", "ras.filter", "ras.filter_id", "ras.release_id", "ras.rejections", "ras.timestamp").
|
||||||
Column(sq.Alias(countQuery, "page_total")).
|
Column(sq.Alias(countQuery, "page_total")).
|
||||||
From("release r").
|
From("release r").
|
||||||
|
@ -245,17 +245,22 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
|
||||||
var rls domain.Release
|
var rls domain.Release
|
||||||
var ras domain.ReleaseActionStatus
|
var ras domain.ReleaseActionStatus
|
||||||
|
|
||||||
var rlsindexer, rlsfilter, infoUrl, downloadUrl sql.NullString
|
var rlsindexer, rlsfilter, infoUrl, downloadUrl, codec sql.NullString
|
||||||
|
|
||||||
var rasId, rasFilterId, rasReleaseId, rasActionId sql.NullInt64
|
var rasId, rasFilterId, rasReleaseId, rasActionId sql.NullInt64
|
||||||
var rasStatus, rasAction, rasType, rasClient, rasFilter sql.NullString
|
var rasStatus, rasAction, rasType, rasClient, rasFilter sql.NullString
|
||||||
var rasRejections []sql.NullString
|
var rasRejections []sql.NullString
|
||||||
var rasTimestamp sql.NullTime
|
var rasTimestamp sql.NullTime
|
||||||
|
|
||||||
if err := rows.Scan(&rls.ID, &rls.FilterStatus, pq.Array(&rls.Rejections), &rlsindexer, &rlsfilter, &rls.Protocol, &infoUrl, &downloadUrl, &rls.Title, &rls.TorrentName, &rls.Size, &rls.Timestamp, &rasId, &rasStatus, &rasAction, &rasActionId, &rasType, &rasClient, &rasFilter, &rasFilterId, &rasReleaseId, pq.Array(&rasRejections), &rasTimestamp, &countItems); err != nil {
|
if err := rows.Scan(&rls.ID, &rls.FilterStatus, pq.Array(&rls.Rejections), &rlsindexer, &rlsfilter, &rls.Protocol, &infoUrl, &downloadUrl, &rls.Title, &rls.TorrentName, &rls.Size, &rls.Category, &rls.Season, &rls.Episode, &rls.Year, &rls.Resolution, &rls.Source, &codec, &rls.Container, &rls.Group, &rls.Timestamp, &rasId, &rasStatus, &rasAction, &rasActionId, &rasType, &rasClient, &rasFilter, &rasFilterId, &rasReleaseId, pq.Array(&rasRejections), &rasTimestamp, &countItems); err != nil {
|
||||||
return res, 0, 0, errors.Wrap(err, "error scanning row")
|
return res, 0, 0, errors.Wrap(err, "error scanning row")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//for _, codec := range codecs {
|
||||||
|
// rls.Codec = append(rls.Codec, codec.String)
|
||||||
|
//
|
||||||
|
//}
|
||||||
|
|
||||||
ras.ID = rasId.Int64
|
ras.ID = rasId.Int64
|
||||||
ras.Status = domain.ReleasePushStatus(rasStatus.String)
|
ras.Status = domain.ReleasePushStatus(rasStatus.String)
|
||||||
ras.Action = rasAction.String
|
ras.Action = rasAction.String
|
||||||
|
@ -291,6 +296,7 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
|
||||||
rls.ActionStatus = make([]domain.ReleaseActionStatus, 0)
|
rls.ActionStatus = make([]domain.ReleaseActionStatus, 0)
|
||||||
rls.InfoURL = infoUrl.String
|
rls.InfoURL = infoUrl.String
|
||||||
rls.DownloadURL = downloadUrl.String
|
rls.DownloadURL = downloadUrl.String
|
||||||
|
rls.Codec = strings.Split(codec.String, ",")
|
||||||
|
|
||||||
// only add ActionStatus if it's not empty
|
// only add ActionStatus if it's not empty
|
||||||
if ras.ID > 0 {
|
if ras.ID > 0 {
|
||||||
|
|
|
@ -59,7 +59,7 @@ type Release struct {
|
||||||
TorrentTmpFile string `json:"-"`
|
TorrentTmpFile string `json:"-"`
|
||||||
TorrentDataRawBytes []byte `json:"-"`
|
TorrentDataRawBytes []byte `json:"-"`
|
||||||
TorrentHash string `json:"-"`
|
TorrentHash string `json:"-"`
|
||||||
TorrentName string `json:"torrent_name"` // full release name
|
TorrentName string `json:"name"` // full release name
|
||||||
Size uint64 `json:"size"`
|
Size uint64 `json:"size"`
|
||||||
Title string `json:"title"` // Parsed title
|
Title string `json:"title"` // Parsed title
|
||||||
Description string `json:"-"`
|
Description string `json:"-"`
|
||||||
|
@ -538,32 +538,6 @@ func (r *Release) HasMagnetUri() bool {
|
||||||
return r.MagnetURI != ""
|
return r.MagnetURI != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
type magnetRoundTripper struct{}
|
|
||||||
|
|
||||||
func (rt *magnetRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
||||||
if r.URL.Scheme == "magnet" {
|
|
||||||
responseBody := r.URL.String()
|
|
||||||
respReader := io.NopCloser(strings.NewReader(responseBody))
|
|
||||||
|
|
||||||
resp := &http.Response{
|
|
||||||
Status: http.StatusText(http.StatusOK),
|
|
||||||
StatusCode: http.StatusOK,
|
|
||||||
Body: respReader,
|
|
||||||
ContentLength: int64(len(responseBody)),
|
|
||||||
Header: map[string][]string{
|
|
||||||
"Content-Type": {"text/plain"},
|
|
||||||
"Location": {responseBody},
|
|
||||||
},
|
|
||||||
Proto: "HTTP/2.0",
|
|
||||||
ProtoMajor: 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.DefaultTransport.RoundTrip(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Release) ResolveMagnetUri(ctx context.Context) error {
|
func (r *Release) ResolveMagnetUri(ctx context.Context) error {
|
||||||
if r.MagnetURI == "" {
|
if r.MagnetURI == "" {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -7,33 +7,110 @@ import * as React from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { formatDistanceToNowStrict } from "date-fns";
|
import { formatDistanceToNowStrict } from "date-fns";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { CellProps } from "react-table";
|
||||||
import { ArrowPathIcon, CheckIcon } from "@heroicons/react/24/solid";
|
import { ArrowPathIcon, CheckIcon } from "@heroicons/react/24/solid";
|
||||||
import { ArrowDownTrayIcon, ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
|
|
||||||
import { ExternalLink } from "../ExternalLink";
|
import { ExternalLink } from "../ExternalLink";
|
||||||
import { ClockIcon, XMarkIcon, NoSymbolIcon } from "@heroicons/react/24/outline";
|
import {
|
||||||
|
ClockIcon,
|
||||||
|
XMarkIcon,
|
||||||
|
NoSymbolIcon,
|
||||||
|
ArrowDownTrayIcon,
|
||||||
|
ArrowTopRightOnSquareIcon, DocumentTextIcon
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { classNames, simplifyDate } from "@utils";
|
import {classNames, humanFileSize, simplifyDate} from "@utils";
|
||||||
import { filterKeys } from "@screens/filters/List";
|
import { filterKeys } from "@screens/filters/List";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { RingResizeSpinner } from "@components/Icons";
|
import { RingResizeSpinner } from "@components/Icons";
|
||||||
import { Tooltip } from "@components/tooltips/Tooltip";
|
import { Tooltip } from "@components/tooltips/Tooltip";
|
||||||
|
|
||||||
interface CellProps {
|
export const NameCell = (props: CellProps<Release>) => (
|
||||||
value: string;
|
<div
|
||||||
}
|
className={classNames(
|
||||||
|
"flex justify-between items-center py-2 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
|
||||||
interface LinksCellProps {
|
"max-w-[82px] sm:max-w-[160px] md:max-w-[290px] lg:max-w-[535px] xl:max-w-[775px]"
|
||||||
value: Release;
|
)}
|
||||||
}
|
>
|
||||||
|
<div className="flex flex-col truncate">
|
||||||
export const AgeCell = ({ value }: CellProps) => (
|
<span className="truncate">
|
||||||
<div className="text-sm text-gray-500" title={simplifyDate(value)}>
|
{String(props.cell.value)}
|
||||||
{formatDistanceToNowStrict(new Date(value), { addSuffix: false })}
|
</span>
|
||||||
|
<div className="text-xs truncate">
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">Category:</span> {props.row.original.category}
|
||||||
|
<span
|
||||||
|
className="text-xs text-gray-500 dark:text-gray-400"> Size:</span> {humanFileSize(props.row.original.size)}
|
||||||
|
<span
|
||||||
|
className="text-xs text-gray-500 dark:text-gray-400"> Misc:</span> {`${props.row.original.resolution} ${props.row.original.source} ${props.row.original.codec ?? ""} ${props.row.original.container}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const IndexerCell = ({ value }: CellProps) => (
|
export const LinksCell = (props: CellProps<Release>) => {
|
||||||
|
return (
|
||||||
|
<div className="flex space-x-2 text-blue-400 dark:text-blue-500">
|
||||||
|
<div>
|
||||||
|
<Tooltip
|
||||||
|
requiresClick
|
||||||
|
label={<DocumentTextIcon
|
||||||
|
className="h-5 w-5 cursor-pointer text-blue-400 hover:text-blue-500 dark:text-blue-500 dark:hover:text-blue-600"
|
||||||
|
aria-hidden={true}/>}
|
||||||
|
title="Details"
|
||||||
|
>
|
||||||
|
<div className="mb-1">
|
||||||
|
<CellLine title="Release">{props.row.original.name}</CellLine>
|
||||||
|
<CellLine title="Indexer">{props.row.original.indexer}</CellLine>
|
||||||
|
<CellLine title="Protocol">{props.row.original.protocol}</CellLine>
|
||||||
|
<CellLine title="Implementation">{props.row.original.implementation}</CellLine>
|
||||||
|
<CellLine title="Category">{props.row.original.category}</CellLine>
|
||||||
|
<CellLine title="Uploader">{props.row.original.uploader}</CellLine>
|
||||||
|
<CellLine title="Size">{humanFileSize(props.row.original.size)}</CellLine>
|
||||||
|
<CellLine title="Title">{props.row.original.title}</CellLine>
|
||||||
|
{props.row.original.year > 0 && <CellLine title="Year">{props.row.original.year.toString()}</CellLine>}
|
||||||
|
{props.row.original.season > 0 &&
|
||||||
|
<CellLine title="Season">{props.row.original.season.toString()}</CellLine>}
|
||||||
|
{props.row.original.episode > 0 &&
|
||||||
|
<CellLine title="Episode">{props.row.original.episode.toString()}</CellLine>}
|
||||||
|
<CellLine title="Resolution">{props.row.original.resolution}</CellLine>
|
||||||
|
<CellLine title="Source">{props.row.original.source}</CellLine>
|
||||||
|
<CellLine title="Codec">{props.row.original.codec}</CellLine>
|
||||||
|
<CellLine title="HDR">{props.row.original.hdr}</CellLine>
|
||||||
|
<CellLine title="Group">{props.row.original.group}</CellLine>
|
||||||
|
<CellLine title="Container">{props.row.original.container}</CellLine>
|
||||||
|
<CellLine title="Origin">{props.row.original.origin}</CellLine>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{props.row.original.download_url && (
|
||||||
|
<ExternalLink href={props.row.original.download_url}>
|
||||||
|
<ArrowDownTrayIcon
|
||||||
|
title="Download torrent file"
|
||||||
|
className="h-5 w-5 hover:text-blue-500 dark:hover:text-blue-600"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</ExternalLink>
|
||||||
|
)}
|
||||||
|
{props.row.original.info_url && (
|
||||||
|
<ExternalLink href={props.row.original.info_url}>
|
||||||
|
<ArrowTopRightOnSquareIcon
|
||||||
|
title="Visit torrentinfo url"
|
||||||
|
className="h-5 w-5 hover:text-blue-500 dark:hover:text-blue-600"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</ExternalLink>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AgeCell = ({value}: CellProps<Release>) => (
|
||||||
|
<div className="text-sm text-gray-500" title={simplifyDate(value)}>
|
||||||
|
{formatDistanceToNowStrict(new Date(value), {addSuffix: false})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const IndexerCell = ({value}: CellProps<Release>) => (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
|
"py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
|
||||||
|
@ -52,7 +129,7 @@ export const IndexerCell = ({ value }: CellProps) => (
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const TitleCell = ({ value }: CellProps) => (
|
export const TitleCell = ({value}: CellProps<Release>) => (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
|
"py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
|
||||||
|
@ -250,19 +327,3 @@ export const ReleaseStatusCell = ({ value }: ReleaseStatusCellProps) => (
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const LinksCell = ({ value }: LinksCellProps) => {
|
|
||||||
return (
|
|
||||||
<div className="flex space-x-2 text-blue-400 dark:text-blue-500">
|
|
||||||
{value.download_url && (
|
|
||||||
<ExternalLink href={value.download_url}>
|
|
||||||
<ArrowDownTrayIcon title="Download torrent file" className="h-5 w-5 hover:text-blue-500 dark:hover:text-blue-600" aria-hidden="true" />
|
|
||||||
</ExternalLink>
|
|
||||||
)}
|
|
||||||
{value.info_url && (
|
|
||||||
<ExternalLink href={value.info_url}>
|
|
||||||
<ArrowTopRightOnSquareIcon title="Visit torrentinfo url" className="h-5 w-5 hover:text-blue-500 dark:hover:text-blue-600" aria-hidden="true" />
|
|
||||||
</ExternalLink>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -4,4 +4,4 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { Button, PageButton } from "./Buttons";
|
export { Button, PageButton } from "./Buttons";
|
||||||
export { AgeCell, IndexerCell, TitleCell, ReleaseStatusCell, LinksCell } from "./Cells";
|
export { AgeCell, IndexerCell, NameCell, TitleCell, ReleaseStatusCell, LinksCell } from "./Cells";
|
||||||
|
|
|
@ -168,7 +168,7 @@ export const ActivityTable = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Release",
|
Header: "Release",
|
||||||
accessor: "torrent_name",
|
accessor: "name",
|
||||||
Cell: DataTable.TitleCell
|
Cell: DataTable.TitleCell
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -183,7 +183,7 @@ export const ActivityTable = () => {
|
||||||
Filter: SelectColumnFilter,
|
Filter: SelectColumnFilter,
|
||||||
filter: "includes"
|
filter: "includes"
|
||||||
}
|
}
|
||||||
], []);
|
] as Column[], []);
|
||||||
|
|
||||||
const { isLoading, data } = useSuspenseQuery({
|
const { isLoading, data } = useSuspenseQuery({
|
||||||
queryKey: ["dash_recent_releases"],
|
queryKey: ["dash_recent_releases"],
|
||||||
|
@ -213,7 +213,7 @@ export const ActivityTable = () => {
|
||||||
const randomNames = RandomLinuxIsos(data.data.length);
|
const randomNames = RandomLinuxIsos(data.data.length);
|
||||||
const newData: Release[] = data.data.map((item, index) => ({
|
const newData: Release[] = data.data.map((item, index) => ({
|
||||||
...item,
|
...item,
|
||||||
torrent_name: `${randomNames[index]}.iso`,
|
name: `${randomNames[index]}.iso`,
|
||||||
indexer: index % 2 === 0 ? "distrowatch" : "linuxtracker"
|
indexer: index % 2 === 0 ? "distrowatch" : "linuxtracker"
|
||||||
}));
|
}));
|
||||||
setModifiedData(newData);
|
setModifiedData(newData);
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { CellProps, Column, useFilters, usePagination, useSortBy, useTable } from "react-table";
|
import { Column, useFilters, usePagination, useSortBy, useTable } from "react-table";
|
||||||
import {
|
import {
|
||||||
ChevronDoubleLeftIcon,
|
ChevronDoubleLeftIcon,
|
||||||
ChevronDoubleRightIcon,
|
ChevronDoubleRightIcon,
|
||||||
|
@ -23,8 +23,6 @@ import * as Icons from "@components/Icons";
|
||||||
import * as DataTable from "@components/data-table";
|
import * as DataTable from "@components/data-table";
|
||||||
|
|
||||||
import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./Filters";
|
import { IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter } from "./Filters";
|
||||||
import { classNames } from "@utils";
|
|
||||||
import { Tooltip } from "@components/tooltips/Tooltip";
|
|
||||||
|
|
||||||
export const releaseKeys = {
|
export const releaseKeys = {
|
||||||
all: ["releases"] as const,
|
all: ["releases"] as const,
|
||||||
|
@ -94,27 +92,8 @@ export const ReleaseTable = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Release",
|
Header: "Release",
|
||||||
accessor: "torrent_name",
|
accessor: "name",
|
||||||
Cell: (props: CellProps<Release>) => {
|
Cell: DataTable.NameCell,
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
"flex justify-between py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
|
|
||||||
"max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
requiresClick
|
|
||||||
label={props.cell.value}
|
|
||||||
maxWidth="max-w-[90vw]"
|
|
||||||
>
|
|
||||||
<span className="whitespace-pre-wrap break-words">
|
|
||||||
{String(props.cell.value)}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
Filter: SearchColumnFilter
|
Filter: SearchColumnFilter
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -156,7 +135,7 @@ export const ReleaseTable = () => {
|
||||||
const randomNames = RandomLinuxIsos(data.data.length);
|
const randomNames = RandomLinuxIsos(data.data.length);
|
||||||
const newData: Release[] = data.data.map((item, index) => ({
|
const newData: Release[] = data.data.map((item, index) => ({
|
||||||
...item,
|
...item,
|
||||||
torrent_name: `${randomNames[index]}.iso`,
|
name: `${randomNames[index]}.iso`,
|
||||||
indexer: index % 2 === 0 ? "distrowatch" : "linuxtracker"
|
indexer: index % 2 === 0 ? "distrowatch" : "linuxtracker"
|
||||||
}));
|
}));
|
||||||
setModifiedData(newData);
|
setModifiedData(newData);
|
||||||
|
|
16
web/src/types/Release.d.ts
vendored
16
web/src/types/Release.d.ts
vendored
|
@ -10,11 +10,27 @@ interface Release {
|
||||||
indexer: string;
|
indexer: string;
|
||||||
filter: string;
|
filter: string;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
|
implementation: string;
|
||||||
|
name: string;
|
||||||
title: string;
|
title: string;
|
||||||
size: number;
|
size: number;
|
||||||
raw: string;
|
raw: string;
|
||||||
info_url: string;
|
info_url: string;
|
||||||
download_url: string;
|
download_url: string;
|
||||||
|
category: string;
|
||||||
|
group: string;
|
||||||
|
season: number;
|
||||||
|
episode: number;
|
||||||
|
year: number;
|
||||||
|
resolution: string;
|
||||||
|
codec: string;
|
||||||
|
source: string;
|
||||||
|
container: string;
|
||||||
|
hdr: string;
|
||||||
|
uploader: string;
|
||||||
|
origin: string;
|
||||||
|
// freeleech: boolean;
|
||||||
|
// freeleech_percent:number;
|
||||||
timestamp: Date
|
timestamp: Date
|
||||||
action_status: ReleaseActionStatus[]
|
action_status: ReleaseActionStatus[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,34 @@ export const get = <T> (obj: T, path: string|Array<any>, defValue?: string) => {
|
||||||
return result === undefined ? defValue : result;
|
return result === undefined ? defValue : result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte', 'petabyte']
|
||||||
|
const BYTES_PER_KB = 1000
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format bytes as human-readable text.
|
||||||
|
*
|
||||||
|
* @param sizeBytes Number of bytes.
|
||||||
|
*
|
||||||
|
* @return Formatted string.
|
||||||
|
*/
|
||||||
|
export function humanFileSize(sizeBytes: number | bigint): string {
|
||||||
|
let size = Math.abs(Number(sizeBytes))
|
||||||
|
|
||||||
|
let u = 0
|
||||||
|
while (size >= BYTES_PER_KB && u < UNITS.length - 1) {
|
||||||
|
size /= BYTES_PER_KB
|
||||||
|
++u
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.NumberFormat([], {
|
||||||
|
style: 'unit',
|
||||||
|
unit: UNITS[u],
|
||||||
|
unitDisplay: 'short',
|
||||||
|
maximumFractionDigits: 1,
|
||||||
|
}).format(size)
|
||||||
|
}
|
||||||
|
|
||||||
export const RandomLinuxIsos = (count: number) => {
|
export const RandomLinuxIsos = (count: number) => {
|
||||||
const linuxIsos = [
|
const linuxIsos = [
|
||||||
"ubuntu-20.04.4-lts-focal-fossa-desktop-amd64-secure-boot",
|
"ubuntu-20.04.4-lts-focal-fossa-desktop-amd64-secure-boot",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue