Subscription management and bandwidth accounting

This commit is contained in:
2021-11-22 21:34:14 +01:00
parent 1c75f68812
commit 206dc5e906
10 changed files with 216 additions and 156 deletions

View File

@@ -23,7 +23,7 @@
<a href="/api">API</a>
<a href="/acknowledgements">Acknowledgements</a>
<a href="https://stats.uptimerobot.com/p9v2ktzyjm" target="_blank">Server Status</a>
{{if eq .User.Subscription.DisableAdDisplay false}}
{{if eq .User.Subscription.ID ""}}
<a href="https://pixeldrain.com/vouchercodes">Shopping discounts</a>
{{end}}
</div>

View File

@@ -5,14 +5,14 @@ import Chart from "../util/Chart.svelte";
let graphViews
let graphBandwidth
let graphHotlink
let graphBandwidthPaid
let graphTimeout = null
let start_time = ""
let end_time = ""
let total_bandwidth = 0
let total_views = 0
let total_hotlink = 0
let total_bandwidth_paid = 0
const loadGraph = (minutes, interval, live) => {
if (graphTimeout !== null) { clearTimeout(graphTimeout) }
if (live) {
@@ -44,17 +44,17 @@ const loadGraph = (minutes, interval, live) => {
graphViews.chart().data.datasets[0].data = resp.views.amounts;
graphBandwidth.chart().data.labels = resp.views.timestamps;
graphBandwidth.chart().data.datasets[0].data = resp.bandwidth.amounts;
graphHotlink.chart().data.labels = resp.views.timestamps;
graphHotlink.chart().data.datasets[0].data = resp.direct_link_bandwidth.amounts;
graphBandwidthPaid.chart().data.labels = resp.views.timestamps;
graphBandwidthPaid.chart().data.datasets[0].data = resp.bandwidth_paid.amounts;
graphViews.update()
graphBandwidth.update()
graphHotlink.update()
graphBandwidthPaid.update()
start_time = resp.views.timestamps[0]
end_time = resp.views.timestamps.slice(-1)[0];
total_bandwidth = resp.bandwidth.amounts.reduce((acc, val) => acc + val)
total_views = resp.views.amounts.reduce((acc, val) => acc + val)
total_hotlink = resp.direct_link_bandwidth.amounts.reduce((acc, val) => acc + val)
total_bandwidth_paid = resp.bandwidth_paid.amounts.reduce((acc, val) => acc + val)
})
}
@@ -141,13 +141,13 @@ onDestroy(() => {
</div>
<Chart bind:this={graphBandwidth} dataType="bytes" label="Bandwidth" />
<hr/>
<Chart bind:this={graphHotlink} dataType="bytes" label="Hotlink bandwidth" />
<Chart bind:this={graphBandwidthPaid} dataType="bytes" label="Paid bandwidth" />
<hr/>
<Chart bind:this={graphViews} dataType="number" label="Views" />
<div class="highlight_dark">
Total usage from {start_time} to {end_time}<br/>
{formatDataVolume(total_bandwidth, 3)} bandwidth,
{formatDataVolume(total_hotlink, 3)} hotlink bandwidth and
{formatDataVolume(total_bandwidth_paid, 3)} paid bandwidth and
{formatThousands(total_views, 3)} views
</div>

View File

@@ -45,6 +45,9 @@ let update_charts = () => {
resp.bandwidth.amounts.forEach((val, idx) => {
resp.bandwidth.amounts[idx] = Math.round(val / file.size);
});
resp.bandwidth_paid.amounts.forEach((val, idx) => {
resp.bandwidth.amounts[idx] += Math.round(val / file.size);
});
download_chart.chart().data.labels = resp.views.timestamps
view_chart.chart().data.labels = resp.views.timestamps
download_chart.chart().data.datasets[0].data = resp.bandwidth.amounts
@@ -78,8 +81,20 @@ let update_charts = () => {
<td>{formatDataVolume(file.size, 4)} ( {formatThousands(file.size)} B )</td>
</tr>
<tr>
<td>Bandwidth</td>
<td>{formatDataVolume(file.bandwidth_used, 4)} ( {formatThousands(file.bandwidth_used)} B )</td>
<td>Free bandwidth used</td>
<td>
{formatDataVolume(file.bandwidth_used, 4)}
( {formatThousands(file.bandwidth_used)} B ),
{(file.bandwidth_used/file.size).toFixed(1)}x file size
</td>
</tr>
<tr>
<td>Premium bandwidth used</td>
<td>
{formatDataVolume(file.bandwidth_used_paid, 4)}
( {formatThousands(file.bandwidth_used_paid)} B ),
{(file.bandwidth_used_paid/file.size).toFixed(1)}x file size
</td>
</tr>
<tr style="border-bottom: none">
<td>Unique downloads</td>

View File

@@ -50,7 +50,7 @@ let update_stats = (id) => {
if (file.size === 0) {
downloads = j.downloads
} else {
downloads = Math.round(j.bandwidth / file.size)
downloads = Math.round((j.bandwidth + j.bandwidth_paid) / file.size)
}
}
socket.onerror = err => {

View File

@@ -437,7 +437,7 @@ const keyboard_event = evt => {
</FilePreview>
</div>
{#if current_file.show_ads && window.viewer_data.user_ads_enabled}
{#if current_file.show_ads}
<AdSkyscraper on:visibility={e => {skyscraper_visible = e.detail}}></AdSkyscraper>
{/if}

View File

@@ -9,13 +9,13 @@ import Euro from "../util/Euro.svelte"
let graph_view = null
let graph_download = null
let graph_bandwidth = null
let graph_direct_link = null
let graph_transfer_paid = null
let time_start = ""
let time_end = ""
let total_views = 0
let total_downloads = 0
let total_bandwidth = 0
let total_direct_link = 0
let total_transfer_paid = 0
let load_graph = (graph, stat, minutes, interval) => {
let today = new Date()
@@ -54,8 +54,8 @@ let load_graph = (graph, stat, minutes, interval) => {
total_downloads = total;
} else if (stat == "bandwidth") {
total_bandwidth = total;
} else if (stat == "direct_bandwidth") {
total_direct_link = total;
} else if (stat == "transfer_paid") {
total_transfer_paid = total;
}
}).catch(e => {
console.error("Error requesting time series: " + e);
@@ -75,11 +75,12 @@ let update_graphs = (minutes, interval, live) => {
load_graph(graph_view, "views", minutes, interval)
load_graph(graph_download, "downloads", minutes, interval)
load_graph(graph_bandwidth, "bandwidth", minutes, interval)
load_graph(graph_direct_link, "direct_bandwidth", minutes, interval)
load_graph(graph_transfer_paid, "transfer_paid", minutes, interval)
load_direct_bw()
}
let direct_link_bandwidth_used = 0
let transfer_cap = 0
let transfer_used = 0
let storage_space_used = 0
let load_direct_bw = () => {
let today = new Date()
@@ -87,7 +88,7 @@ let load_direct_bw = () => {
start.setDate(start.getDate() - 30)
fetch(
window.api_endpoint + "/user/time_series/direct_bandwidth" +
window.api_endpoint + "/user/time_series/transfer_paid" +
"?start=" + start.toISOString() +
"&end=" + today.toISOString() +
"&interval=60"
@@ -96,7 +97,7 @@ let load_direct_bw = () => {
return resp.json();
}).then(resp => {
let total = resp.amounts.reduce((accum, val) => accum += val, 0);
direct_link_bandwidth_used = total
transfer_used = total
storage_space_used = window.user.storage_space_used
}).catch(e => {
console.error("Error requesting time series: " + e);
@@ -104,6 +105,14 @@ let load_direct_bw = () => {
}
onMount(() => {
if (window.user.subscription.monthly_transfer_cap > 0) {
transfer_cap = window.user.subscription.monthly_transfer_cap
} else if (window.user.monthly_transfer_cap > 0) {
transfer_cap = window.user.monthly_transfer_cap
} else {
transfer_cap = -1
}
update_graphs(1440, 1, true);
})
onDestroy(() => {
@@ -128,14 +137,6 @@ onDestroy(() => {
<li>
Max file size: {formatDataVolume(window.user.subscription.file_size_limit, 3)}
</li>
<li>
Advertisements when you view files:
{#if window.user.subscription.disable_ad_display}No{:else}Yes{/if}
</li>
<li>
Advertisements when others view your files:
{#if window.user.subscription.disable_ads_on_files}No{:else}Yes{/if}
</li>
{#if window.user.subscription.file_expiry_days > 0}
<li>Files expire after {window.user.subscription.file_expiry_days} days</li>
{:else}
@@ -156,10 +157,10 @@ onDestroy(() => {
<StorageProgressBar used={storage_space_used} total={window.user.subscription.storage_space}></StorageProgressBar>
{/if}
{#if window.user.subscription.direct_linking_bandwidth === -1}
Hotlink bandwidth used in the last 30 days: {formatDataVolume(direct_link_bandwidth_used, 3)}<br/>
{#if transfer_cap === -1}
Paid transfers in the last 30 days: {formatDataVolume(transfer_used, 3)}<br/>
{:else}
<HotlinkProgressBar used={direct_link_bandwidth_used} total={window.user.subscription.direct_linking_bandwidth}></HotlinkProgressBar>
<HotlinkProgressBar used={transfer_used} total={transfer_cap}></HotlinkProgressBar>
{/if}
<h3>Exports</h3>
@@ -224,8 +225,18 @@ onDestroy(() => {
{formatThousands(total_views)} views,
{formatThousands(total_downloads)} downloads,
{formatDataVolume(total_bandwidth, 3)} bandwidth and
{formatDataVolume(total_direct_link, 3)} direct link bandwidth
{formatDataVolume(total_transfer_paid, 3)} paid transfers
</div>
<div class="limit_width">
<h3>Paid transfers</h3>
<p>
A paid transfer is when a file is downloaded using the data cap on
your subscription plan. These can be files you downloaded from other
people, or other people downloading your files if you have bandwidth
sharing enabled.
</p>
</div>
<Chart bind:this={graph_transfer_paid} dataType="bytes" label="Paid transfers" />
<div class="limit_width">
<h3>Views</h3>
<p>
@@ -255,17 +266,4 @@ onDestroy(() => {
</p>
</div>
<Chart bind:this={graph_bandwidth} dataType="bytes" label="Bandwidth" />
<div class="limit_width">
<h3>Hotlink bandwidth</h3>
<p>
When a file is downloaded without going through pixeldrain's
download page it counts as a hotlink. Because hotlinking costs us
bandwidth and doesn't generate any ad revenue we have to limit it.
When your hotlink bandwidth runs out people will be asked to do a
test before they can download your files. See our
<a href="/#pro">subscription options</a> to get more hotlink
bandwidth.
</p>
</div>
<Chart bind:this={graph_direct_link} dataType="bytes" label="Hotlink bandwidth" />
</div>

View File

@@ -8,7 +8,7 @@ $: frac = used / total
</script>
<div>
Hotlink bandwidth:
Paid transfers:
{formatDataVolume(used, 3)}
out of
{formatDataVolume(total, 3)}
@@ -20,10 +20,9 @@ $: frac = used / total
{#if frac > 0.99}
<div class="highlight_red">
You have used all of your hotlink bandwidth. Other people won't
be able to download your files directly from the API anymore.
Downloads will have to go through the file viewer page. Please
upgrade to a higher support tier to continue hotlinking files:
You have used all of your data cap. People can still download your
files, but not directly from the API anymore. The file viewer shows
ads on your files and download speeds are limited.
<br/>
<a class="button button_highlight" href="https://www.patreon.com/join/pixeldrain">
Upgrade options
@@ -31,12 +30,10 @@ $: frac = used / total
</div>
{:else if frac > 0.8}
<div class="highlight_yellow">
You have used {(frac*100).toFixed(0)}% of your
hotlink bandwidth. If your hotlink bandwidth runs out people
won't be able to download your files directly from the API
anymore. Downloads will have to go through the file viewer page.
Please upgrade to a higher support tier to continue hotlinking
files:
You have used {(frac*100).toFixed(0)}% of your data cap. If your
data runs out people won't be able to download your files directly
from the API anymore, ads will be shown on the file viewer and
transfer rates will be limited.
<br/>
<a class="button button_highlight" href="https://www.patreon.com/join/pixeldrain">
Upgrade options

View File

@@ -55,6 +55,13 @@ onMount(() => {
<i class="icon">vpn_key</i>
API keys
</a>
<a class="button"
href="/user/subscription"
class:button_highlight={page === "subscription"}
on:click|preventDefault={() => {navigate("subscription", "Subscription")}}>
<i class="icon">shopping_cart</i>
Subscription
</a>
{#if window.user.balance_micro_eur !== 0}
<a class="button"
href="/user/transactions"
@@ -63,13 +70,6 @@ onMount(() => {
<i class="icon">receipt_long</i>
Transactions
</a>
<a class="button"
href="/user/subscription"
class:button_highlight={page === "subscription"}
on:click|preventDefault={() => {navigate("subscription", "Subscription")}}>
<i class="icon">shopping_cart</i>
Subscription
</a>
{/if}
</div>

View File

@@ -3,15 +3,20 @@ import Spinner from "../util/Spinner.svelte";
import Euro from "../util/Euro.svelte"
let loading = false
let subscription = window.user.subscription.id
let hotlinking = window.user.hotlinking_enabled
let transfer_cap = window.user.monthly_transfer_cap / 1e12
let result = ""
let result_success = false
const update_subscription = async name => {
const update_subscription = async () => {
loading = true
const form = new FormData()
form.append("subscription", name)
form.append("subscription", subscription)
form.append("hotlinking_enabled", hotlinking)
form.append("transfer_cap", transfer_cap*1e12)
try {
const resp = await fetch(
window.api_endpoint+"/user/subscription",
@@ -62,97 +67,135 @@ const update_subscription = async name => {
balance to activate the subscription again.
</p>
{#if result !== ""}
<div class:highlight_green={result_success} class:highlight_red={!result_success}>
{result}
<h3>Prepaid plans</h3>
{#if window.user.subscription.type === "patreon"}
<p>Prepaid subscriptions are not available for Patreon supporters.</p>
{:else}
{#if result !== ""}
<div class:highlight_green={result_success} class:highlight_red={!result_success}>
{result}
</div>
{/if}
<div class="feat_table">
<div>
<div class="feat_label" class:feat_highlight={subscription === "prepaid"}>
Prepaid<br/>
{#if subscription === "prepaid"}
Currently active
{:else}
<button on:click={() => {subscription = "prepaid"; update_subscription()}}>
<i class="icon">attach_money</i>
Activate
</button>
{/if}
</div>
<div class="feat_normal round_tr">
<ul>
<li>Base price of €1 per month</li>
<li>€4 per TB per month for storage</li>
<li>€2 per TB for data transfer</li>
<li>Files never expire as long as subscription is active</li>
</ul>
</div>
</div>
<div>
<div class="feat_label" class:feat_highlight={subscription === "prepaid_temp_storage_120d"}>
120 days storage<br/>
{#if subscription === "prepaid_temp_storage_120d"}
Currently active
{:else}
<button on:click={() => {subscription = "prepaid_temp_storage_120d"; update_subscription()}}>
<i class="icon">attach_money</i>
Activate
</button>
{/if}
</div>
<div class="feat_normal">
<ul>
<li>Base price of €1 per month</li>
<li>€2 per TB per month for storage</li>
<li>€2 per TB for data transfer</li>
<li>Files expire 120 days after the last time they're viewed</li>
</ul>
</div>
</div>
<div>
<div class="feat_label" class:feat_highlight={subscription === "prepaid_temp_storage_60d"}>
60 days storage<br/>
{#if subscription === "prepaid_temp_storage_60d"}
Currently active
{:else}
<button on:click={() => {subscription = "prepaid_temp_storage_60d"; update_subscription()}}>
<i class="icon">attach_money</i>
Activate
</button>
{/if}
</div>
<div class="feat_normal">
<ul>
<li>Base price of €1 per month</li>
<li>€1 per TB per month for storage</li>
<li>€2 per TB for data transfer</li>
<li>Files expire 60 days after the last time they're viewed</li>
</ul>
</div>
</div>
<div>
<div class="feat_label" class:feat_highlight={subscription === ""}>
Free<br/>
{#if subscription === ""}
Currently active
{:else}
<button on:click={() => {subscription = ""; update_subscription()}}>
<i class="icon">money_off</i>
Activate
</button>
{/if}
</div>
<div class="feat_normal round_br">
<ul>
<li>Standard free plan, files expire after 30 days.</li>
</ul>
</div>
</div>
</div>
{/if}
<div class="feat_table">
<div>
<div class="feat_label" class:feat_highlight={window.user.subscription.id === "prepaid"}>
Prepaid<br/>
{#if window.user.subscription.id === "prepaid"}
Currently active
{:else}
<button on:click={() => {update_subscription("prepaid")}}>
<i class="icon">attach_money</i>
Activate
</button>
{/if}
</div>
<div class="feat_normal round_tr">
<ul>
<li>Base price of €2 per month (includes all the benefits of the Pro plan)</li>
<li>€4 per TB per month for storage</li>
<li>€2 per TB for hotlink bandwidth</li>
<li>All advertisements disabled</li>
<li>No rate limit, your files will download at the highest speed possible</li>
</ul>
</div>
</div>
<div>
<div class="feat_label" class:feat_highlight={window.user.subscription.id === "prepaid_storage"}>
Just storage<br/>
{#if window.user.subscription.id === "prepaid_storage"}
Currently active
{:else}
<button on:click={() => {update_subscription("prepaid_storage")}}>
<i class="icon">attach_money</i>
Activate
</button>
{/if}
</div>
<div class="feat_normal">
<ul>
<li>Base price of €1 per month</li>
<li>€4 per TB per month for storage</li>
<li>100 GB of hotlink bandwidth per month</li>
<li>You don't see ads, but people downloading your files do see ads</li>
</ul>
</div>
</div>
<div>
<div class="feat_label" class:feat_highlight={window.user.subscription.id === "prepaid_storage_temp"}>
Temporary storage<br/>
{#if window.user.subscription.id === "prepaid_storage_temp"}
Currently active
{:else}
<button on:click={() => {update_subscription("prepaid_storage_temp")}}>
<i class="icon">attach_money</i>
Activate
</button>
{/if}
</div>
<div class="feat_normal">
<ul>
<li>Base price of €1 per month</li>
<li>€2 per TB per month for storage</li>
<li>100 GB of hotlink bandwidth per month</li>
<li>You don't see ads, but people downloading your files do see ads</li>
<li>Files expire 90 days after the last time they're viewed</li>
</ul>
</div>
</div>
<div>
<div class="feat_label" class:feat_highlight={window.user.subscription.id === ""}>
Free<br/>
{#if window.user.subscription.id === ""}
Currently active
{:else}
<button on:click={() => {update_subscription("none")}}>
<i class="icon">money_off</i>
Activate
</button>
{/if}
</div>
<div class="feat_normal round_br">
<ul>
<li>Standard free plan, files expire after 30 days.</li>
</ul>
</div>
</div>
</div>
<h3>Bandwidth sharing</h3>
{#if hotlinking}
<button on:click={() => { hotlinking = false; update_subscription() }}>
<i class="icon green">check</i> ON (click to turn off)
</button>
{:else}
<button on:click={() => { hotlinking = true; update_subscription() }}>
<i class="icon red">close</i> OFF (click to turn on)
</button>
{/if}
<p>
When bandwidth sharing is enabled all the bandwidth that your files
use will be subtracted from your data cap. Advertisements will be
disabled on the download pages for your files and download speed
will be unlimited. The rate limiting captcha for files is also
disabled when bandwidth sharing is on. You can directly embed your
file's download link anywhere, you don't need to use the file viewer
page.
</p>
<h3>Bill shock limit</h3>
Billshock limit in terabytes per month. Set to 0 to disable<br/>
<input type="number" bind:value={transfer_cap}/> TB
<button on:click={update_subscription}>
<i class="icon">save</i> Save
</button>
<p>
The billshock limit limits how much bandwidth your account can use
in a 30 day window. When this limit is reached files will show ads
again and can only be downloaded from the file viewer page. This is
mostly useful for prepaid plans, but it works for patreon plans too.
Set to 0 to disable the limit.
</p>
</div>
</div>
@@ -204,4 +247,11 @@ const update_subscription = async name => {
.feat_table > div > div.round_tr { border-top-right-radius: 0.5em; }
.feat_table > div > div.round_br { border-bottom-right-radius: 0.5em; }
.green {
color: var(--highlight_color);
}
.red {
color: var(--danger_color);
}
</style>

View File

@@ -74,7 +74,7 @@ func (wc *WebController) serveFileViewer(w http.ResponseWriter, r *http.Request,
var vd = fileViewerData{
CaptchaKey: wc.captchaKey(),
ViewToken: wc.viewTokenOrBust(),
UserAdsEnabled: !(templateData.Authenticated && templateData.User.Subscription.DisableAdDisplay),
UserAdsEnabled: templateData.User.Subscription.ID == "",
}
if len(ids) > 1 {
@@ -149,7 +149,7 @@ func (wc *WebController) serveListViewer(w http.ResponseWriter, r *http.Request,
Type: "list",
CaptchaKey: wc.captchaSiteKey,
ViewToken: wc.viewTokenOrBust(),
UserAdsEnabled: !(templateData.Authenticated && templateData.User.Subscription.DisableAdDisplay),
UserAdsEnabled: templateData.User.Subscription.ID == "",
APIResponse: list,
}