Compare commits

...

20 Commits

Author SHA1 Message Date
Pablo Ferreiro 78fe44070c Fixed Trending and normalized Discover 2023-01-26 19:21:14 +01:00
Pablo Ferreiro 7d2f6e8fd6 bump scraper 2023-01-26 17:20:38 +01:00
Pablo Ferreiro 32d49954f9 hotfix 2023-01-25 21:10:47 +01:00
Pablo Ferreiro 5c770def41 better ui 2023-01-25 21:06:59 +01:00
Pablo Ferreiro d273b35e53 Added apcu caching method and bump 2023-01-25 15:56:23 +01:00
Pablo Ferreiro faf1dcee51 bump, video showing up 2023-01-25 15:18:46 +01:00
Pablo Ferreiro 89d9ad2f9c Mobile-friendly 2023-01-25 15:00:52 +01:00
Pablo Ferreiro 7ddf6f7738 bump cosign 2023-01-25 14:10:35 +01:00
Pablo Ferreiro 059bf7208a bump 2023-01-25 13:59:59 +01:00
Pablo Ferreiro d5858c39fd Fixed undefined vars 2022-12-27 16:58:03 +01:00
Pablo Ferreiro 76be343c13 Merge pull request #114 from TheFrenchGhosty/patch-1
Update the credits, deprecate my image
2022-12-27 14:37:27 +00:00
TheFrenchGhosty a7b0edf8fe Update the credits, deprecate my image 2022-12-21 17:37:23 +01:00
Pablo Ferreiro 73e9d7a1cd Dynamically generated manifest 2022-11-27 14:26:35 +01:00
Pablo Ferreiro 0224cd25eb Added url_tag 2022-11-27 00:05:11 +01:00
Pablo Ferreiro 215f984fe4 Properly implemented OG 2022-11-26 23:51:45 +01:00
Pablo Ferreiro ee6e8b0593 Revert "Added better og support"
This reverts commit fa3f1e0a7c.
2022-11-26 23:36:20 +01:00
Pablo Ferreiro fa3f1e0a7c Added better og support 2022-11-26 23:28:59 +01:00
Pablo Ferreiro ce5c9e016b Better profile pic resolution 2022-11-26 22:25:48 +01:00
Pablo Ferreiro 49fcc79f15 bump 2022-11-22 20:45:53 +01:00
Pablo Ferreiro b138f4bccc misc folder and date on /video 2022-11-18 16:48:51 +01:00
44 changed files with 320 additions and 200 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: pablouser1
assignees: ''
---
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@main
with:
cosign-release: 'v1.13.1'
cosign-release: 'v2.0.0-rc.0'
# Workaround: https://github.com/docker/build-push-action/issues/461
+1 -1
View File
@@ -1 +1 @@
web: vendor/bin/heroku-php-nginx -C setup/heroku/nginx.conf
web: vendor/bin/heroku-php-nginx -C misc/setup/heroku/nginx.conf
+2 -1
View File
@@ -40,7 +40,8 @@ Apply to: Main window (address bar)
* Add custom amount of videos per page
## Credits
[@TheFrenchGhosty](https://github.com/TheFrenchGhosty): Initial Dockerfile and fixes to a usable state. You can check his Docker image [here](https://github.com/PussTheCat-org/docker-proxitok-quay) on Github or [here](https://quay.io/repository/pussthecatorg/proxitok) on Quay
[TheFrenchGhosty](https://thefrenchghosty.me) ([Github](https://github.com/TheFrenchGhosty)): Initial Dockerfile and fixes to a usable state.
### External libraries
* [TikScraperPHP](https://github.com/pablouser1/TikScraperPHP)
* [Latte](https://github.com/nette/latte)
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Cache;
use TikScraper\Interfaces\CacheInterface;
class ApcuCache implements CacheInterface {
function __construct() {
if (!(extension_loaded('apcu') && apcu_enabled())) {
throw new \Exception('APCu not enabled');
}
}
public function get(string $cache_key): ?object {
$data = apcu_fetch($cache_key);
return $data !== false ? json_decode($data) : null;
}
public function exists(string $cache_key): bool {
return apcu_exists($cache_key);
}
public function set(string $cache_key, string $data, $timeout = 3600): void {
apcu_store($cache_key, $data, $timeout);
}
}
+1
View File
@@ -3,5 +3,6 @@ namespace App\Constants;
abstract class CacheMethods {
const JSON = 'json';
const APCU = 'apcu';
const REDIS = 'redis';
}
+5 -5
View File
@@ -3,16 +3,16 @@ namespace App\Controllers;
use App\Helpers\ErrorHandler;
use App\Helpers\Wrappers;
use App\Models\FeedTemplate;
use App\Models\DiscoverTemplate;
class DiscoverController {
static public function get() {
$api = Wrappers::api();
$feed = $api->discover();
if ($feed->meta->success) {
Wrappers::latte('discover', new FeedTemplate('Discover', $feed));
$data = $api->discover();
if ($data->meta->success) {
Wrappers::latte('discover', new DiscoverTemplate($data));
} else {
ErrorHandler::showMeta($feed->meta);
ErrorHandler::showMeta($data->meta);
}
}
}
+3 -2
View File
@@ -11,8 +11,9 @@ class EmbedController {
$video = $api->video($id);
$video->feed();
if ($video->ok()) {
$data = $video->getFull();
Wrappers::latte('video', new VideoTemplate($data->feed->items[0], $data->info->detail, true));
$item = $video->getFeed()->items[0];
$info = $video->getInfo();
Wrappers::latte('video', new VideoTemplate($item, $info, true));
} else {
ErrorHandler::showMeta($video->error());
}
+3 -2
View File
@@ -14,8 +14,9 @@ class MusicController {
$music = $api->music($music_id);
$music->feed($cursor);
if ($music->ok()) {
$data = $music->getFull();
Wrappers::latte('music', new FullTemplate('Music', $data));
$info = $music->getInfo();
$feed = $music->getFeed();
Wrappers::latte('music', new FullTemplate('Music', $info, $feed));
} else {
ErrorHandler::showMeta($music->error());
}
+30 -30
View File
@@ -2,42 +2,12 @@
namespace App\Controllers;
use App\Helpers\Cookies;
use TikScraper\Helpers\Converter;
class ProxyController {
const VALID_TIKTOK_DOMAINS = [
"tiktokcdn.com", "tiktokcdn-us.com", "tiktok.com"
];
static private function isValidDomain(string $url) {
$host = parse_url($url, PHP_URL_HOST);
$host_split = explode('.', $host);
$host_count = count($host_split);
if ($host_count === 2) {
// Using no watermark
return in_array($host_split[0] . '.' . $host_split[1], self::VALID_TIKTOK_DOMAINS);
} elseif ($host_count === 3) {
return in_array($host_split[1] . '.' . $host_split[2], self::VALID_TIKTOK_DOMAINS);
}
return false;
}
static private function checkUrl() {
if (!isset($_GET['url'])) {
die('You need to send a URL');
}
if (!filter_var($_GET['url'], FILTER_VALIDATE_URL) || !self::isValidDomain($_GET['url'])) {
die('Not a valid URL');
}
}
static private function getFilename(string $id, string $user): string {
$filename = 'tiktok-video-' . $id . '-' . $user;
return $filename;
}
static public function stream() {
self::checkUrl();
$url = $_GET['url'];
@@ -60,4 +30,34 @@ class ProxyController {
// Running
$downloader->url($url, $filename, $watermark);
}
static private function isValidDomain(string $url): bool {
$valid = false;
$host = parse_url($url, PHP_URL_HOST);
$host_split = explode('.', $host);
$host_count = count($host_split);
if ($host_count === 2) {
// Using no watermark
$valid = in_array($host_split[0] . '.' . $host_split[1], self::VALID_TIKTOK_DOMAINS);
} elseif ($host_count === 3) {
$valid = in_array($host_split[1] . '.' . $host_split[2], self::VALID_TIKTOK_DOMAINS);
}
return $valid;
}
static private function checkUrl(): void {
if (!isset($_GET['url'])) {
die('You need to send a URL');
}
if (!filter_var($_GET['url'], FILTER_VALIDATE_URL) || !self::isValidDomain($_GET['url'])) {
die('Not a valid URL');
}
}
static private function getFilename(string $id, string $user): string {
$filename = 'tiktok-video-' . $id . '-' . $user;
return $filename;
}
}
+20 -1
View File
@@ -2,12 +2,13 @@
namespace App\Controllers;
use App\Helpers\ErrorHandler;
use App\Helpers\Misc;
use App\Helpers\UrlBuilder;
/**
* Used to be compatible with HTML forms
*/
class RedirectController {
static public function redirect() {
static public function search() {
$endpoint = '/';
if (isset($_GET['type'], $_GET['term'])) {
$term = trim($_GET['term']);
@@ -48,6 +49,24 @@ class RedirectController {
header("Location: {$url}");
}
static public function download() {
if (!(isset($_GET['videoId'], $_GET['authorUsername'], $_GET['playAddr']))) {
ErrorHandler::showText(400, 'Request incomplete');
return;
}
$watermark = isset($_GET['watermark']) && $_GET['watermark'] === 'yes' ? true : false;
$url = '';
if ($watermark) {
$url = UrlBuilder::download($_GET['playAddr'], $_GET['authorUsername'], $_GET['videoId'], true);
} else {
$url = UrlBuilder::download(UrlBuilder::video_external($_GET['authorUsername'], $_GET['videoId']), $_GET['authorUsername'], $_GET['videoId'], false);
}
header("Location: {$url}");
}
/**
* to_endpoint maps a TikTok URL into a ProxiTok-compatible endpoint URL.
*/
+3 -2
View File
@@ -15,8 +15,9 @@ class TagController {
$hashtag = $api->hashtag($name);
$hashtag->feed($cursor);
if ($hashtag->ok()) {
$data = $hashtag->getFull();
Wrappers::latte('tag', new FullTemplate($data->info->detail->title, $data));
$info = $hashtag->getInfo();
$feed = $hashtag->getFeed();
Wrappers::latte('tag', new FullTemplate($info->detail->title, $info, $feed));
} else {
ErrorHandler::showMeta($hashtag->error());
}
+7 -5
View File
@@ -16,12 +16,13 @@ class UserController {
$user = $api->user($username);
$user->feed($cursor);
if ($user->ok()) {
$data = $user->getFull();
if ($data->info->detail->privateAccount) {
$info = $user->getInfo();
$feed = $user->getFeed();
if ($info->detail->privateAccount) {
ErrorHandler::showText(401, "Private account detected! Not supported");
return;
}
Wrappers::latte('user', new FullTemplate($data->info->detail->nickname, $data));
Wrappers::latte('user', new FullTemplate($info->detail->nickname, $info, $feed));
} else {
ErrorHandler::showMeta($user->error());
}
@@ -32,8 +33,9 @@ class UserController {
$video = $api->video($video_id);
$video->feed();
if ($video->ok()) {
$data = $video->getFull();
Wrappers::latte('video', new VideoTemplate($data->feed->items[0], $data->info->detail));
$item = $video->getFeed()->items[0];
$info = $video->getInfo();
Wrappers::latte('video', new VideoTemplate($item, $info));
} else {
ErrorHandler::showMeta($video->error());
}
+7
View File
@@ -1,6 +1,7 @@
<?php
namespace App\Helpers;
use App\Cache\ApcuCache;
use App\Cache\JSONCache;
use App\Cache\RedisCache;
use App\Constants\CacheMethods;
@@ -76,6 +77,9 @@ class Wrappers {
$latte->addFunction('url_user', function (string $username): string {
return UrlBuilder::user($username);
});
$latte->addFunction('url_tag', function (string $tag): string {
return UrlBuilder::tag($tag);
});
$latte->addFunction('url_video_internal', function (string $username, string $id): string {
return UrlBuilder::video_internal($username, $id);
});
@@ -135,6 +139,9 @@ class Wrappers {
case CacheMethods::JSON:
$cacheEngine = new JSONCache();
break;
case CacheMethods::APCU:
$cacheEngine = new ApcuCache();
break;
case CacheMethods::REDIS:
if (!(isset($_ENV['REDIS_URL']) || isset($_ENV['REDIS_HOST'], $_ENV['REDIS_PORT']))) {
throw new \Exception('You need to set REDIS_URL or REDIS_HOST and REDIS_PORT to use Redis Cache!');
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Models;
use TikScraper\Models\Discover;
class DiscoverTemplate extends BaseTemplate {
public Discover $data;
function __construct(Discover $data) {
parent::__construct("Discover");
$this->data = $data;
}
}
+5 -5
View File
@@ -1,16 +1,16 @@
<?php
namespace App\Models;
use TikScraper\Models\Feed;
/**
* Base for templates with a feed
*/
class FeedTemplate extends BaseTemplate {
public object $data;
public Feed $feed;
function __construct(string $title, object $feed) {
function __construct(string $title, Feed $feed) {
parent::__construct($title);
$this->data = (object) [
'feed' => $feed
];
$this->feed = $feed;
}
}
+7 -4
View File
@@ -1,16 +1,19 @@
<?php
namespace App\Models;
use TikScraper\Models\Full;
use TikScraper\Models\Feed;
use TikScraper\Models\Info;
/**
* Base for templates with both info and feed
*/
class FullTemplate extends BaseTemplate {
public Full $data;
public Info $info;
public Feed $feed;
function __construct(string $title, Full $data) {
function __construct(string $title, Info $info, Feed $feed) {
parent::__construct($title);
$this->data = $data;
$this->info = $info;
$this->feed = $feed;
}
}
+5 -3
View File
@@ -1,18 +1,20 @@
<?php
namespace App\Models;
use TikScraper\Models\Info;
/**
* Base for templates with a feed
*/
class VideoTemplate extends BaseTemplate {
public object $item;
public object $detail;
public Info $info;
public string $layout = 'hero';
function __construct(object $item, object $detail, bool $isEmbed = false) {
function __construct(object $item, Info $info, bool $isEmbed = false) {
parent::__construct('Video');
$this->item = $item;
$this->detail = $detail;
$this->info = $info;
if ($isEmbed) {
$this->layout = 'embed';
} else {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "pablouser1/proxitok",
"description": "An alternative frontend for TikTok",
"version": "2.4.3.0",
"version": "2.4.5.0",
"license": "AGPL-3.0-or-later",
"type": "project",
"authors": [
Generated
+27 -27
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7dc0bdf9fcecc4c3a73fb2fb1eece6f4",
"content-hash": "818a3e623705c076822f3d895a0d99ca",
"packages": [
{
"name": "bramus/router",
@@ -263,16 +263,16 @@
},
{
"name": "pablouser1/tikscraper",
"version": "v2.3.2.1",
"version": "v2.3.4.0",
"source": {
"type": "git",
"url": "https://github.com/pablouser1/TikScraperPHP.git",
"reference": "b67340e62cda1fb117b518e4f812e327aec094d6"
"reference": "8ee7efd58892d0a6e536b7cc29de200202916394"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pablouser1/TikScraperPHP/zipball/b67340e62cda1fb117b518e4f812e327aec094d6",
"reference": "b67340e62cda1fb117b518e4f812e327aec094d6",
"url": "https://api.github.com/repos/pablouser1/TikScraperPHP/zipball/8ee7efd58892d0a6e536b7cc29de200202916394",
"reference": "8ee7efd58892d0a6e536b7cc29de200202916394",
"shasum": ""
},
"require": {
@@ -305,9 +305,9 @@
"description": "Get data from TikTok API",
"support": {
"issues": "https://github.com/pablouser1/TikScraperPHP/issues",
"source": "https://github.com/pablouser1/TikScraperPHP/tree/v2.3.2.1"
"source": "https://github.com/pablouser1/TikScraperPHP/tree/v2.3.4.0"
},
"time": "2022-11-04T18:45:45+00:00"
"time": "2023-01-26T18:18:34+00:00"
},
{
"name": "php-webdriver/webdriver",
@@ -406,16 +406,16 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.26.0",
"version": "v1.27.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e"
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
"shasum": ""
},
"require": {
@@ -430,7 +430,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.26-dev"
"dev-main": "1.27-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -469,7 +469,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
},
"funding": [
{
@@ -485,20 +485,20 @@
"type": "tidelift"
}
],
"time": "2022-05-24T11:49:31+00:00"
"time": "2022-11-03T14:55:06+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.26.0",
"version": "v1.27.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
"shasum": ""
},
"require": {
@@ -507,7 +507,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.26-dev"
"dev-main": "1.27-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -552,7 +552,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0"
},
"funding": [
{
@@ -568,20 +568,20 @@
"type": "tidelift"
}
],
"time": "2022-05-10T07:21:04+00:00"
"time": "2022-11-03T14:55:06+00:00"
},
{
"name": "symfony/process",
"version": "v5.4.11",
"version": "v5.4.19",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1"
"reference": "c5ba874c9b636dbccf761e22ce750e88ec3f55e1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/6e75fe6874cbc7e4773d049616ab450eff537bf1",
"reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1",
"url": "https://api.github.com/repos/symfony/process/zipball/c5ba874c9b636dbccf761e22ce750e88ec3f55e1",
"reference": "c5ba874c9b636dbccf761e22ce750e88ec3f55e1",
"shasum": ""
},
"require": {
@@ -614,7 +614,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v5.4.11"
"source": "https://github.com/symfony/process/tree/v5.4.19"
},
"funding": [
{
@@ -630,7 +630,7 @@
"type": "tidelift"
}
],
"time": "2022-06-27T16:58:25+00:00"
"time": "2023-01-01T08:32:19+00:00"
}
],
"packages-dev": [],
+1
View File
@@ -28,6 +28,7 @@ $bulmaswatch-import-font: false;
@import "./node_modules/bulma/sass/form/tools.sass";
// Components
@import "./node_modules/bulma/sass/components/breadcrumb.sass";
@import "./node_modules/bulma/sass/components/card.sass";
@import "./node_modules/bulma/sass/components/dropdown.sass";
@import "./node_modules/bulma/sass/components/media.sass";
+2 -2
View File
@@ -2,8 +2,8 @@
"name": "proxitok-scss",
"private": true,
"scripts": {
"bulma": "sass --style=compressed bulma.scss ../styles/vendor/bulma.min.css",
"cssgg": "sass --style=compressed cssgg.scss ../styles/vendor/cssgg.min.css"
"bulma": "sass --style=compressed bulma.scss ../../styles/vendor/bulma.min.css",
"cssgg": "sass --style=compressed cssgg.scss ../../styles/vendor/cssgg.min.css"
},
"dependencies": {
"bulma": "^0.9.3",
+6 -6
View File
@@ -3,9 +3,9 @@
anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
@@ -118,9 +118,9 @@ readdirp@~3.6.0:
picomatch "^2.2.1"
sass@^1.46.0:
version "1.55.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.55.0.tgz#0c4d3c293cfe8f8a2e8d3b666e1cf1bff8065d1c"
integrity sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==
version "1.56.1"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.1.tgz#94d3910cd468fd075fa87f5bb17437a0b617d8a7"
integrity sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
+32 -1
View File
@@ -2,6 +2,7 @@
/** @var \Bramus\Router\Router $router */
use App\Helpers\ErrorHandler;
use App\Helpers\Misc;
use App\Helpers\Wrappers;
use App\Models\BaseTemplate;
@@ -21,9 +22,39 @@ $router->get('/verify', function () {
Wrappers::latte('verify', new BaseTemplate('verify'));
});
$router->get('/manifest', function () {
header('Content-Type: application/json');
$data = [
"name" => "ProxiTok",
"short_name" => "ProxiTok",
"description" => "Use TikTok with a privacy-friendly alternative frontend",
"lang" => "en-US",
"theme_color" => "#4040ff",
"background_color" => "#ffffff",
"display" => "standalone",
"orientation" => "portrait-primary",
"icons" => [
[
"src" => Misc::url('/android-chrome-192x192.png'),
"sizes" => "192x192",
"type" => "image/png"
],
[
"src" => Misc::url('/android-chrome-512x512.png'),
"sizes" => "512x512",
"type" => "image/png"
]
],
"start_url" => Misc::url('/'),
"scope" => Misc::url('/')
];
echo json_encode($data, JSON_PRETTY_PRINT);
});
$router->get('/stream', 'ProxyController@stream');
$router->get('/download', 'ProxyController@download');
$router->get('/redirect', 'RedirectController@redirect');
$router->get('/redirect/search', 'RedirectController@search');
$router->get('/redirect/download', 'RedirectController@download');
$router->mount('/trending', function () use ($router) {
$router->get('/', 'TrendingController@get');
-23
View File
@@ -1,23 +0,0 @@
{
"name": "ProxiTok",
"short_name": "ProxiTok",
"description": "Use TikTok with a privacy-friendly alternative frontend",
"orientation": "portrait-primary",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"id": "./",
"start_url": "./",
"theme_color": "#4040ff",
"background_color": "#ffffff",
"display": "standalone"
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["../../scss/node_modules/css.gg/icons/scss/home.scss","../../scss/node_modules/css.gg/icons/scss/options.scss","../../scss/node_modules/css.gg/icons/scss/info.scss","../../scss/node_modules/css.gg/icons/scss/code-slash.scss","../../scss/node_modules/css.gg/icons/scss/search.scss","../../scss/node_modules/css.gg/icons/scss/feed.scss","../../scss/node_modules/css.gg/icons/scss/eye.scss","../../scss/node_modules/css.gg/icons/scss/heart.scss","../../scss/node_modules/css.gg/icons/scss/comment.scss","../../scss/node_modules/css.gg/icons/scss/share.scss","../../scss/node_modules/css.gg/icons/scss/software-download.scss","../../scss/node_modules/css.gg/icons/scss/lock.scss","../../scss/node_modules/css.gg/icons/scss/lock-unlock.scss","../../scss/node_modules/css.gg/icons/scss/music.scss"],"names":[],"mappings":"AAAA,SACE,yLACA,sBACA,kBACA,cACA,+BACA,WACA,YACA,iBACA,aACA,gBACA,4BACA,2BACA,6BACA,4BACA,mBAEA,gBACE,WACA,cACA,sBACA,kBAGF,iBACE,WACA,cACA,sBACA,kBACA,qBACA,sBACA,2BACA,wBACA,SACA,kBACA,WACA,YACA,OAGF,gBACE,UACA,YACA,iBACA,oBACA,4BACA,6BACA,gBACA,SACA,SCjDJ,YACI,sBACA,kBACA,cACA,+BACA,WACA,WACA,qCAEA,mBACE,WACA,cACA,sBACA,kBACA,UACA,WACA,iBACA,mBAGF,oBACE,WACA,cACA,sBACA,kBACA,UACA,WACA,iBACA,mBACA,SACA,UAGF,mBACE,YACA,WCnCN,SACI,sBACA,kBACA,cACA,+BACA,WACA,YACA,iBACA,mBAEA,iCACE,WACA,cACA,sBACA,kBACA,kBACA,UACA,wBACA,SAGF,gBACE,WACA,WAGF,iBACE,WACA,QC5BN,eACI,cACA,kBACA,sBACA,6CACA,UACA,YACA,wBAEA,sBACE,WACA,cACA,sBACA,kBACA,UACA,WACA,yBAGF,uBACE,WACA,cACA,sBACA,kBACA,UACA,WACA,yBACA,sBACA,qBACA,UACA,QAGF,sBACE,uBACA,wBACA,WACA,QCrCN,WACI,sBACA,kBACA,cACA,+BACA,WACA,YACA,iBACA,mBACA,iBACA,gBAEA,kBACE,WACA,cACA,sBACA,kBACA,kBACA,UACA,WACA,wBACA,yBACA,SACA,UCvBN,SACI,cACA,sBACA,wBACA,sBASA,iBACA,gBACA,kBACA,+BACA,UACA,WACA,kBAbA,iCACE,cACA,sBACA,wBACA,sBAWF,iCACE,WACA,kBACA,kBACA,WACA,WACA,QACA,SACA,WAGF,gBACE,UACA,QACA,WCnCN,QACI,kBACA,cACA,+BACA,WACA,YACA,iCACA,gCACA,gBACA,sBAEA,+BACE,WACA,cACA,oBACA,kBACA,sBAGF,eACE,QACA,8CACA,WACA,YAGF,gBACE,UACA,WACA,iBACA,WACA,SC/BN,UACI,iBACA,6BACA,8BACA,WACA,WACA,gBAeA,sBACA,kBACA,gHACA,cAhBA,iBACE,iBACA,6BACA,8BACA,WACA,WACA,gBACA,WACA,cACA,sBACA,kBAQF,kBACE,WACA,cACA,sBACA,kBAGF,iBACE,WACA,wBACA,QAGF,kBACE,WACA,YACA,sBACA,wBACA,UACA,QC7CN,YACI,sBACA,kBACA,cACA,+BACA,WACA,YACA,iBACA,gBACA,0CAEA,mBACE,WACA,cACA,sBACA,kBACA,UAGF,oBACE,WACA,cACA,sBACA,kBACA,UACA,iBACA,+BACA,+BACA,UACA,YACA,WAGF,mBACE,WACA,wBACA,qBACA,SACA,QCtCN,UACI,sBACA,kBACA,cACA,+BACA,UACA,WACA,wBACA,oBACA,kCAEA,iBACE,WACA,cACA,sBACA,kBACA,kBACA,WACA,WACA,wBACA,SAGF,kBACE,WACA,cACA,sBACA,kBACA,kBACA,WACA,WACA,wBACA,SACA,MACA,yBAGF,iBACE,SACA,wBCvCN,sBACI,sBACA,kBACA,cACA,+BACA,WACA,WACA,iBACA,aACA,8BACA,+BACA,eAEA,6BACE,WACA,cACA,sBACA,kBACA,UACA,WACA,sBACA,wBACA,yBACA,SACA,WAGF,8BACE,WACA,cACA,sBACA,kBACA,kBACA,UACA,YACA,wBACA,SACA,WCrCN,SACI,sBACA,kBACA,cACA,+BACA,WACA,YACA,iBACA,4BACA,2BACA,4BACA,iBAEA,gBACE,WACA,cACA,sBACA,kBACA,WACA,YACA,kBACA,+BACA,qBACA,UACA,QCxBN,gBACI,sBACA,kBACA,cACA,+BACA,WACA,WACA,iBACA,8BACA,6BACA,4BACA,iBACA,iBAEA,+CACE,WACA,cACA,sBACA,kBAGF,uBACE,+BACA,qBACA,WACA,YACA,kBACA,UACA,SAGF,wBACE,UACA,WACA,wBACA,UACA,QCpCN,UACI,cACA,sBACA,iBAQA,2BACA,4BACA,gBACA,gBACA,kBACA,6CACA,WACA,YAbA,mCACE,cACA,sBACA,iBAYF,mCACE,WACA,kBACA,UACA,WACA,QACA,UACA,uBACA,mBAGF,iBACE","file":"cssgg.min.css"}
{"version":3,"sourceRoot":"","sources":["../../misc/scss/node_modules/css.gg/icons/scss/home.scss","../../misc/scss/node_modules/css.gg/icons/scss/options.scss","../../misc/scss/node_modules/css.gg/icons/scss/info.scss","../../misc/scss/node_modules/css.gg/icons/scss/code-slash.scss","../../misc/scss/node_modules/css.gg/icons/scss/search.scss","../../misc/scss/node_modules/css.gg/icons/scss/feed.scss","../../misc/scss/node_modules/css.gg/icons/scss/eye.scss","../../misc/scss/node_modules/css.gg/icons/scss/heart.scss","../../misc/scss/node_modules/css.gg/icons/scss/comment.scss","../../misc/scss/node_modules/css.gg/icons/scss/share.scss","../../misc/scss/node_modules/css.gg/icons/scss/software-download.scss","../../misc/scss/node_modules/css.gg/icons/scss/lock.scss","../../misc/scss/node_modules/css.gg/icons/scss/lock-unlock.scss","../../misc/scss/node_modules/css.gg/icons/scss/music.scss"],"names":[],"mappings":"AAAA,SACE,yLACA,sBACA,kBACA,cACA,+BACA,WACA,YACA,iBACA,aACA,gBACA,4BACA,2BACA,6BACA,4BACA,mBAEA,gBACE,WACA,cACA,sBACA,kBAGF,iBACE,WACA,cACA,sBACA,kBACA,qBACA,sBACA,2BACA,wBACA,SACA,kBACA,WACA,YACA,OAGF,gBACE,UACA,YACA,iBACA,oBACA,4BACA,6BACA,gBACA,SACA,SCjDJ,YACI,sBACA,kBACA,cACA,+BACA,WACA,WACA,qCAEA,mBACE,WACA,cACA,sBACA,kBACA,UACA,WACA,iBACA,mBAGF,oBACE,WACA,cACA,sBACA,kBACA,UACA,WACA,iBACA,mBACA,SACA,UAGF,mBACE,YACA,WCnCN,SACI,sBACA,kBACA,cACA,+BACA,WACA,YACA,iBACA,mBAEA,iCACE,WACA,cACA,sBACA,kBACA,kBACA,UACA,wBACA,SAGF,gBACE,WACA,WAGF,iBACE,WACA,QC5BN,eACI,cACA,kBACA,sBACA,6CACA,UACA,YACA,wBAEA,sBACE,WACA,cACA,sBACA,kBACA,UACA,WACA,yBAGF,uBACE,WACA,cACA,sBACA,kBACA,UACA,WACA,yBACA,sBACA,qBACA,UACA,QAGF,sBACE,uBACA,wBACA,WACA,QCrCN,WACI,sBACA,kBACA,cACA,+BACA,WACA,YACA,iBACA,mBACA,iBACA,gBAEA,kBACE,WACA,cACA,sBACA,kBACA,kBACA,UACA,WACA,wBACA,yBACA,SACA,UCvBN,SACI,cACA,sBACA,wBACA,sBASA,iBACA,gBACA,kBACA,+BACA,UACA,WACA,kBAbA,iCACE,cACA,sBACA,wBACA,sBAWF,iCACE,WACA,kBACA,kBACA,WACA,WACA,QACA,SACA,WAGF,gBACE,UACA,QACA,WCnCN,QACI,kBACA,cACA,+BACA,WACA,YACA,iCACA,gCACA,gBACA,sBAEA,+BACE,WACA,cACA,oBACA,kBACA,sBAGF,eACE,QACA,8CACA,WACA,YAGF,gBACE,UACA,WACA,iBACA,WACA,SC/BN,UACI,iBACA,6BACA,8BACA,WACA,WACA,gBAeA,sBACA,kBACA,gHACA,cAhBA,iBACE,iBACA,6BACA,8BACA,WACA,WACA,gBACA,WACA,cACA,sBACA,kBAQF,kBACE,WACA,cACA,sBACA,kBAGF,iBACE,WACA,wBACA,QAGF,kBACE,WACA,YACA,sBACA,wBACA,UACA,QC7CN,YACI,sBACA,kBACA,cACA,+BACA,WACA,YACA,iBACA,gBACA,0CAEA,mBACE,WACA,cACA,sBACA,kBACA,UAGF,oBACE,WACA,cACA,sBACA,kBACA,UACA,iBACA,+BACA,+BACA,UACA,YACA,WAGF,mBACE,WACA,wBACA,qBACA,SACA,QCtCN,UACI,sBACA,kBACA,cACA,+BACA,UACA,WACA,wBACA,oBACA,kCAEA,iBACE,WACA,cACA,sBACA,kBACA,kBACA,WACA,WACA,wBACA,SAGF,kBACE,WACA,cACA,sBACA,kBACA,kBACA,WACA,WACA,wBACA,SACA,MACA,yBAGF,iBACE,SACA,wBCvCN,sBACI,sBACA,kBACA,cACA,+BACA,WACA,WACA,iBACA,aACA,8BACA,+BACA,eAEA,6BACE,WACA,cACA,sBACA,kBACA,UACA,WACA,sBACA,wBACA,yBACA,SACA,WAGF,8BACE,WACA,cACA,sBACA,kBACA,kBACA,UACA,YACA,wBACA,SACA,WCrCN,SACI,sBACA,kBACA,cACA,+BACA,WACA,YACA,iBACA,4BACA,2BACA,4BACA,iBAEA,gBACE,WACA,cACA,sBACA,kBACA,WACA,YACA,kBACA,+BACA,qBACA,UACA,QCxBN,gBACI,sBACA,kBACA,cACA,+BACA,WACA,WACA,iBACA,8BACA,6BACA,4BACA,iBACA,iBAEA,+CACE,WACA,cACA,sBACA,kBAGF,uBACE,+BACA,qBACA,WACA,YACA,kBACA,UACA,SAGF,wBACE,UACA,WACA,wBACA,UACA,QCpCN,UACI,cACA,sBACA,iBAQA,2BACA,4BACA,gBACA,gBACA,kBACA,6CACA,WACA,YAbA,mCACE,cACA,sBACA,iBAYF,mCACE,WACA,kBACA,UACA,WACA,QACA,UACA,uBACA,mBAGF,iBACE","file":"cssgg.min.css"}
+19 -3
View File
@@ -5,11 +5,27 @@
<link rel="apple-touch-icon" sizes="180x180" href="{path('/apple-touch-icon.png')}">
<link rel="icon" type="image/png" sizes="32x32" href="{path('/favicon-32x32.png')}">
<link rel="icon" type="image/png" sizes="16x16" href="{path('/favicon-16x16.png')}">
<link rel="manifest" href="{path('/site.webmanifest')}">
<link rel="manifest" href="{path('/manifest')}">
{if isset($og, $og_content, $og_url)}
<!-- Using TikTok's meta config -->
<meta property="og:title" content="{$og->title}" />
<meta property="og:description" content="{$og->description}" />
<meta property="og:url" content="{$og_url}" />
<meta property="og:image" content="{url_stream($og_content)}" />
<meta property="twitter:title" content="{$og->title}" />
<meta property="twitter:description" content="{$og->description}" />
<meta property="twitter:image" content="{url_stream($og_content)}" />
{else}
<!-- Using predifined ProxiTok config -->
<meta property="og:title" content="{$title}" />
<meta property="og:description" content="Alternative frontend for TikTok" />
<meta property="twitter:title" content="{$title}" />
<meta property="twitter:description" content="Alternative frontend for TikTok" />
{/if}
<meta property="og:site_name" content="ProxiTok" />
<meta property="og:title" content="{$title}" />
<meta property="og:description" content="Alternative frontend for TikTok" />
<meta property="og:type" content="website" />
<meta property="twitter:card" content="summary" />
<meta property="twitter:site" content="ProxiTok" />
{if isset($has_rss)}
<link rel="alternate" type="application/rss+xml" title="{$title}" href="{$_SERVER['REQUEST_URI'] . '/rss'}" />
{/if}
+3 -3
View File
@@ -1,8 +1,8 @@
<link rel="stylesheet" href="{static('css', 'themes/card.css')}">
<noscript>JavaScript is required for this section to work!</noscript>
<section class="section">
<noscript>JavaScript is required for this section to work!</noscript>
<div class="columns is-multiline is-vcentered">
{foreach $data->feed->items as $item}
{foreach $feed->items as $item}
{do $share_url = url_video_external($item->author->uniqueId, $item->id)}
<div class="column is-one-quarter clickable-img" id="{$item->id}" onclick="openVideo(this.id)"
data-video_url="{url_stream($item->video->playAddr)}"
@@ -17,7 +17,7 @@
<img class="hidden" loading="lazy" data-src="{url_stream($item->video->dynamicCover)}" />
</div>
{/foreach}
{if empty($data->feed->items)}
{if empty($feed->items)}
<p class="title">No items sent by TikTok!</p>
{/if}
</div>
@@ -1,7 +1,7 @@
<div n:ifset="$data->info" class="buttons">
<div class="buttons">
{* is_numeric is used to avoid having a back button with ttwid cursors *}
{if isset($_GET['cursor']) && is_numeric($_GET['cursor']) && $_GET['cursor'] != 0}
<a class="button is-danger" href="?cursor=0">First</a>
{/if}
<a n:attr="disabled => !$data->feed->hasMore" class="button is-success" href="?cursor={$data->feed->maxCursor}">Next</a>
<a n:attr="disabled => !$feed->hasMore" class="button is-success" href="?cursor={$feed->maxCursor}">Next</a>
</div>
@@ -1,13 +1,8 @@
<div class="dropdown is-hoverable">
<div class="dropdown-trigger">
<button class="button is-success" aria-haspopup="true" aria-controls="dropdown-menu">
{include '../../icon.latte', icon: 'software-download', text: 'Download'}
</button>
</div>
<div class="dropdown-menu" role="menu">
<div class="dropdown-content">
<a target="_blank" href="{url_download($playAddr, $uniqueId, $id, true)}" class="dropdown-item">Watermark</a>
<a target="_blank" href="{url_download(url_video_external($uniqueId, $id), $uniqueId, $id, false)}" class="dropdown-item">No watermark</a>
</div>
</div>
<div class="field is-grouped is-grouped-centered">
<p class="control">
<a target="_blank" href="{url_download($playAddr, $uniqueId, $id, true)}" class="button is-info">Watermark</a>
</p>
<p class="control">
<a target="_blank" href="{url_download(url_video_external($uniqueId, $id), $uniqueId, $id, false)}" class="button is-success">No watermark</a>
</p>
</div>
+11 -16
View File
@@ -1,17 +1,12 @@
<div class="dropdown is-hoverable">
<div class="dropdown-trigger">
<button class="button is-primary" aria-haspopup="true" aria-controls="dropdown-menu">
{include '../../icon.latte', icon: 'share', text: 'Share'}
</button>
</div>
<div class="dropdown-menu" role="menu">
<div class="dropdown-content">
<a href="{url_video_internal($uniqueId, $id)}" class="dropdown-item has-text-success">
{include '../../icon.latte', icon: 'lock', text: 'Instance link'}
</a>
<a href="{url_video_external($uniqueId, $id)}" class="dropdown-item has-text-warning">
{include '../../icon.latte', icon: 'lock-unlock', text: 'Original Link'}
</a>
</div>
</div>
<div class="field is-grouped is-grouped-centered">
<p class="control">
<a class="button is-success" href="{url_video_internal($uniqueId, $id)}">
{include '../../icon.latte', icon: 'lock', text: 'Instance'}
</a>
</p>
<p class="control">
<a class="button is-warning" href="{url_video_external($uniqueId, $id)}">
{include '../../icon.latte', icon: 'lock-unlock', text: 'Original'}
</a>
</p>
</div>
+7 -7
View File
@@ -1,9 +1,9 @@
<div class="container">
{foreach $data->feed->items as $item}
{foreach $feed->items as $item}
<article class="media">
<figure class="media-left">
<p class="image is-64x64">
<img src="{url_stream($item->author->avatarThumb)}" />
<img class="is-rounded" src="{url_stream($item->author->avatarThumb)}" />
</p>
</figure>
<div class="media-content">
@@ -24,14 +24,14 @@
<source src="{url_stream($item->video->playAddr)}" type="video/mp4" />
</video>
</div>
<div class="has-text-centered">
{include './common/share.latte', uniqueId: $item->author->uniqueId, id: $item->id}
{include './common/download.latte', playAddr: $item->video->playAddr, id: $item->id, uniqueId: $item->author->uniqueId}
</div>
<p class="has-text-centered"><b>Download video</b></p>
{include './common/download.latte', playAddr: $item->video->playAddr, id: $item->id, uniqueId: $item->author->uniqueId}
<p class="has-text-centered"><b>Share link</b></p>
{include './common/share.latte', uniqueId: $item->author->uniqueId, id: $item->id}
</div>
</article>
{/foreach}
{if empty($data->feed->items)}
{if empty($feed->items)}
<p class="title">No items sent by TikTok!</p>
{/if}
</div>
+1 -1
View File
@@ -9,7 +9,7 @@
{include '../components/navbar.latte'}
</div>
<div class="hero-body">
<div class="container has-text-centered">
<div class="container">
{block content}{/block}
</div>
</div>
+1 -1
View File
@@ -5,7 +5,7 @@
{/block}
{block content}
{foreach $data->feed->items as $type => $items}
{foreach $data->items as $type => $items}
<p class="title">{$type|firstUpper}</p>
<div class="columns is-multiline is-vcentered">
{foreach $items as $item}
+3 -3
View File
@@ -1,9 +1,9 @@
{layout '../layouts/hero.latte'}
{block content}
<p class="title">Welcome to ProxiTok!</p>
<p class="subtitle">An alternative open source frontend for TikTok</p>
{embed '../components/form.latte', path: '/redirect', method: 'GET'}
<p class="title has-text-centered">Welcome to ProxiTok!</p>
<p class="subtitle has-text-centered">An alternative open source frontend for TikTok</p>
{embed '../components/form.latte', path: '/redirect/search', method: 'GET'}
{block fields}
<div class="field has-addons has-addons-centered">
<div class="control">
+3 -3
View File
@@ -1,9 +1,9 @@
{layout '../layouts/default.latte'}
{block header}
<p class="title">{$data->info->detail->title}</p>
<p class="subtitle">{$data->info->detail->desc}</p>
<p>Videos: {number($data->info->stats->videoCount)}</p>
<p class="title">{$info->detail->title}</p>
<p class="subtitle">{$info->detail->desc}</p>
<p>Videos: {number($info->stats->videoCount)}</p>
{/block}
{block content}
+9 -5
View File
@@ -2,15 +2,19 @@
{var $has_rss = true}
{var $og = $info->meta->og}
{var $og_content = $info->detail->profileLarger}
{var $og_url = url_tag($info->detail->title)}
{block header}
{if $data->info->detail->profileThumb !== ''}
<figure class="figure is-96x96">
<img src="{url_stream($data->info->detail->profileThumb)}" />
{if $info->detail->profileLarger !== ''}
<figure class="image is-inline-block is-128x128">
<img class="is-rounded" src="{url_stream($info->detail->profileLarger)}" />
</figure>
{/if}
<p class="title">{$data->info->detail->title}</p>
<p class="title">{$info->detail->title}</p>
<p class="subtitle">{include '../components/rss.latte'}</p>
<p>Videos: {number($data->info->stats->videoCount)} / Views: {number($data->info->stats->viewCount)}</p>
<p>Videos: {number($info->stats->videoCount)} / Views: {number($info->stats->viewCount)}</p>
{/block}
{block content}
+10 -6
View File
@@ -2,15 +2,19 @@
{var $has_rss = true}
{var $og = $info->meta->og}
{var $og_content = $info->detail->avatarLarger}
{var $og_url = url_user($info->detail->uniqueId)}
{block header}
<figure class="figure is-96x96">
<img src="{url_stream($data->info->detail->avatarThumb)}" />
<figure class="image is-inline-block is-128x128">
<img class="is-rounded" src="{url_stream($info->detail->avatarLarger)}" />
</figure>
<p class="title">{$data->info->detail->uniqueId}</p>
<p class="title">{$info->detail->uniqueId}</p>
<p class="subtitle">{include '../components/rss.latte'}</p>
<p>{$data->info->detail->signature}</p>
<p>Following: {number($data->info->stats->followingCount)} / Followers: {number($data->info->stats->followerCount)}</p>
<p>Hearts: {number($data->info->stats->heartCount)} / Videos: {$data->info->stats->videoCount}</p>
<p>{$info->detail->signature}</p>
<p>Following: {number($info->stats->followingCount)} / Followers: {number($info->stats->followerCount)}</p>
<p>Hearts: {number($info->stats->heartCount)} / Videos: {$info->stats->videoCount}</p>
{/block}
{block content}
+33 -12
View File
@@ -1,24 +1,45 @@
{layout "../layouts/{$layout}.latte"}
{var $og = $info->meta->og}
{var $og_content = $item->video->originCover}
{var $og_url = url_video_internal($info->detail->uniqueId, $item->id)}
{block content}
<div class="columns has-text-centered is-centered is-vcentered is-gapless">
<div class="column">
<div class="columns is-centered is-vcentered is-gapless">
<div class="column has-text-centered">
<video controls autoplay poster="{url_stream($item->video->originCover)}">
<source src="{url_stream($item->video->playAddr)}" type="video/mp4" />
</video>
</div>
<div class="column">
<div class="box">
<p class="title">Video by <a href="{url_user($detail->uniqueId)}">{$detail->uniqueId}</a></p>
<p class="subtitle">{$item->desc}</p>
{include '../components/themes/common/stats.latte', playCount: $item->stats->playCount, diggCount: $item->stats->diggCount, commentCount: $item->stats->commentCount, shareCount: $item->stats->shareCount}
<div class="container mt-4">
<p>{include '../components/icon.latte', icon: 'music', text: $item->music->title}</p>
<audio src="{url_stream($item->music->playUrl)}" controls preload="none"></audio>
</div>
<hr />
{include '../components/themes/common/share.latte', uniqueId: $detail->uniqueId, id: $item->id}
{include '../components/themes/common/download.latte', playAddr: $item->video->playAddr, id: $item->id, uniqueId: $detail->uniqueId}
<article class="media">
<figure class="media-left">
<p class="image is-64x64">
<img src="{url_stream($info->detail->avatarThumb)}" />
</p>
</figure>
<div class="media-content">
<p>
<strong>{$info->detail->nickname}</strong>
<small>
<a href="{url_user($info->detail->uniqueId)}">@{$info->detail->uniqueId}</a>
</small>
<small title="{date('M d, Y H:i:s e', $item->createTime)}">{date('M d, Y', $item->createTime)}</small>
</p>
{if !empty($item->challenges)}
<p>{include '../components/themes/common/tags.latte', challenges: $item->challenges}</p>
{/if}
<div class="content">
<p n:ifcontent>{$item->desc}</p>
{include '../components/themes/common/stats.latte', playCount: $item->stats->playCount, diggCount: $item->stats->diggCount, commentCount: $item->stats->commentCount, shareCount: $item->stats->shareCount}
<div class="has-text-centered">
{include '../components/themes/common/share.latte', uniqueId: $info->detail->uniqueId, id: $item->id}
{include '../components/themes/common/download.latte', playAddr: $item->video->playAddr, id: $item->id, uniqueId: $info->detail->uniqueId}
</div>
</div>
</div>
</article>
</div>
</div>
</div>