aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--res/client.js176
-rw-r--r--src/client/IPlayer.hx1
-rw-r--r--src/client/Player.hx38
-rw-r--r--src/client/players/Iframe.hx5
-rw-r--r--src/client/players/Raw.hx17
-rw-r--r--src/client/players/Youtube.hx51
6 files changed, 184 insertions, 104 deletions
diff --git a/res/client.js b/res/client.js
index 9637f8a..c0a61e3 100644
--- a/res/client.js
+++ b/res/client.js
@@ -126,6 +126,16 @@ Lambda.exists = function(it,f) {
}
return false;
};
+Lambda.find = function(it,f) {
+ var v = $getIterator(it);
+ while(v.hasNext()) {
+ var v1 = v.next();
+ if(f(v1)) {
+ return v1;
+ }
+ }
+ return null;
+};
var haxe_ds_StringMap = function() {
this.h = { };
};
@@ -1598,6 +1608,9 @@ var client_Player = function(main) {
this.videoItemsEl = window.document.querySelector("#queue");
this.items = _$VideoList_VideoList_$Impl_$._new();
this.main = main;
+ this.players = [new client_players_Youtube(main,this)];
+ this.iframePlayer = new client_players_Iframe(main,this);
+ this.rawPlayer = new client_players_Raw(main,this);
this.initItemButtons();
};
client_Player.__name__ = true;
@@ -1633,27 +1646,35 @@ client_Player.prototype = {
_$VideoList_VideoList_$Impl_$.toggleItemType(this.items,pos);
this.setItemElementType(this.videoItemsEl.children[pos],this.items[pos].isTemp);
}
- ,setPlayer: function(player) {
- this.player = player;
+ ,setPlayer: function(newPlayer) {
+ if(this.player != null && this.player != newPlayer) {
+ this.player.removeVideo();
+ }
+ this.player = newPlayer;
}
,getVideoData: function(url,callback) {
- if(client_players_Youtube.isYoutube(url)) {
- new client_players_Youtube(this.main,this).getVideoData(url,callback);
- } else {
- new client_players_Raw(this.main,this).getVideoData(url,callback);
+ var player = Lambda.find(this.players,function(player1) {
+ return player1.isSupportedLink(url);
+ });
+ if(player == null) {
+ player = this.rawPlayer;
}
+ player.getVideoData(url,callback);
}
,setVideo: function(i) {
if(!this.main.isSyncActive) {
return;
}
var item = this.items[i];
- if(item.isIframe) {
- this.setPlayer(new client_players_Iframe(this.main,this));
- } else if(client_players_Youtube.isYoutube(item.url)) {
- this.setPlayer(new client_players_Youtube(this.main,this));
+ var currentPlayer = Lambda.find(this.players,function(p) {
+ return p.isSupportedLink(item.url);
+ });
+ if(currentPlayer != null) {
+ this.setPlayer(currentPlayer);
+ } else if(item.isIframe) {
+ this.setPlayer(this.iframePlayer);
} else {
- this.setPlayer(new client_players_Raw(this.main,this));
+ this.setPlayer(this.rawPlayer);
}
var childs = this.videoItemsEl.children;
if(childs[this.itemPos] != null) {
@@ -1662,7 +1683,6 @@ client_Player.prototype = {
this.itemPos = i;
childs[this.itemPos].classList.add("queue_active");
this.currentSrc = item.url;
- this.playerEl.textContent = "";
this.isLoaded = false;
this.player.loadVideo(item);
window.document.querySelector("#currenttitle").textContent = item.title;
@@ -2038,7 +2058,14 @@ var client_players_Iframe = function(main,player) {
};
client_players_Iframe.__name__ = true;
client_players_Iframe.prototype = {
- loadVideo: function(item) {
+ isSupportedLink: function(url) {
+ return true;
+ }
+ ,getVideoData: function(url,callback) {
+ callback({ duration : 356400, title : "Custom Media"});
+ }
+ ,loadVideo: function(item) {
+ this.removeVideo();
this.video = window.document.createElement("div");
this.video.id = "videoplayer";
this.video.innerHTML = item.url;
@@ -2081,7 +2108,10 @@ var client_players_Raw = function(main,player) {
};
client_players_Raw.__name__ = true;
client_players_Raw.prototype = {
- getVideoData: function(url,callback) {
+ isSupportedLink: function(url) {
+ return true;
+ }
+ ,getVideoData: function(url,callback) {
var _gthis = this;
var title = HxOverrides.substr(url,url.lastIndexOf("/") + 1,null);
var matchName = new EReg("^(.+)\\.","");
@@ -2110,22 +2140,27 @@ client_players_Raw.prototype = {
}
,loadVideo: function(item) {
var _gthis = this;
+ var url = this.main.tryLocalIp(item.url);
+ if(this.video != null) {
+ this.video.src = url;
+ return;
+ }
this.video = window.document.createElement("video");
this.video.id = "videoplayer";
- var url = this.main.tryLocalIp(item.url);
this.video.src = url;
this.video.controls = true;
- var isTouch = 'ontouchstart' in window;
- if(client_players_Raw.controlsHider != null) {
- client_players_Raw.controlsHider.stop();
+ if(this.controlsHider != null) {
+ this.controlsHider.stop();
}
- if(!isTouch) {
- client_players_Raw.controlsHider = haxe_Timer.delay(function() {
+ if(!client_Utils.isTouch()) {
+ this.controlsHider = haxe_Timer.delay(function() {
return _gthis.video.controls = false;
},3000);
}
this.video.onmousemove = function(e) {
- client_players_Raw.controlsHider.stop();
+ if(_gthis.controlsHider != null) {
+ _gthis.controlsHider.stop();
+ }
_gthis.video.controls = true;
return _gthis.video.onmousemove = null;
};
@@ -2200,40 +2235,47 @@ var client_players_Youtube = function(main,player) {
this.matchHours = new EReg("([0-9]+)H","");
this.isLoaded = false;
this.playerEl = window.document.querySelector("#ytapiplayer");
+ this.urlVideoId = "?part=snippet&fields=items(snippet/resourceId/videoId)";
+ this.urlTitleDuration = "?part=snippet,contentDetails&fields=items(snippet/title,contentDetails/duration)";
+ this.playlistUrl = "https://www.googleapis.com/youtube/v3/playlistItems";
+ this.videosUrl = "https://www.googleapis.com/youtube/v3/videos";
+ this.matchPlaylist = new EReg("youtube\\.com.*list=([A-z0-9_-]+)","");
+ this.matchEmbed = new EReg("embed/([A-z0-9_-]+)","");
+ this.matchShort = new EReg("youtu.be/([A-z0-9_-]+)","");
+ this.matchId = new EReg("v=([A-z0-9_-]+)","");
this.main = main;
this.player = player;
- client_players_Youtube.apiKey = main.getYoutubeApiKey();
};
client_players_Youtube.__name__ = true;
-client_players_Youtube.isYoutube = function(url) {
- if(client_players_Youtube.extractVideoId(url) == "") {
- return client_players_Youtube.extractPlaylistId(url) != "";
- } else {
- return true;
- }
-};
-client_players_Youtube.extractVideoId = function(url) {
- if(url.indexOf("youtu.be/") != -1) {
- client_players_Youtube.matchShort.match(url);
- return client_players_Youtube.matchShort.matched(1);
- }
- if(url.indexOf("youtube.com/embed/") != -1) {
- client_players_Youtube.matchEmbed.match(url);
- return client_players_Youtube.matchEmbed.matched(1);
+client_players_Youtube.prototype = {
+ isSupportedLink: function(url) {
+ if(this.extractVideoId(url) == "") {
+ return this.extractPlaylistId(url) != "";
+ } else {
+ return true;
+ }
}
- if(!client_players_Youtube.matchId.match(url)) {
- return "";
+ ,extractVideoId: function(url) {
+ if(url.indexOf("youtu.be/") != -1) {
+ this.matchShort.match(url);
+ return this.matchShort.matched(1);
+ }
+ if(url.indexOf("youtube.com/embed/") != -1) {
+ this.matchEmbed.match(url);
+ return this.matchEmbed.matched(1);
+ }
+ if(!this.matchId.match(url)) {
+ return "";
+ }
+ return this.matchId.matched(1);
}
- return client_players_Youtube.matchId.matched(1);
-};
-client_players_Youtube.extractPlaylistId = function(url) {
- if(!client_players_Youtube.matchPlaylist.match(url)) {
- return "";
+ ,extractPlaylistId: function(url) {
+ if(!this.matchPlaylist.match(url)) {
+ return "";
+ }
+ return this.matchPlaylist.matched(1);
}
- return client_players_Youtube.matchPlaylist.matched(1);
-};
-client_players_Youtube.prototype = {
- convertTime: function(duration) {
+ ,convertTime: function(duration) {
var total = 0;
var hours = this.matchHours.match(duration);
var minutes = this.matchMinutes.match(duration);
@@ -2251,12 +2293,15 @@ client_players_Youtube.prototype = {
}
,getVideoData: function(url,callback) {
var _gthis = this;
- var id = client_players_Youtube.extractVideoId(url);
+ if(this.apiKey == null) {
+ this.apiKey = this.main.getYoutubeApiKey();
+ }
+ var id = this.extractVideoId(url);
if(id == "") {
this.getPlaylistVideoData(url,callback);
return;
}
- var http = new haxe_http_HttpJs("" + client_players_Youtube.videosUrl + client_players_Youtube.urlTitleDuration + "&id=" + id + "&key=" + client_players_Youtube.apiKey);
+ var http = new haxe_http_HttpJs("" + this.videosUrl + this.urlTitleDuration + "&id=" + id + "&key=" + this.apiKey);
http.onData = function(data) {
var json = JSON.parse(data);
if(json.error != null) {
@@ -2287,7 +2332,8 @@ client_players_Youtube.prototype = {
}
,getPlaylistVideoData: function(url,callback) {
var _gthis = this;
- var http = new haxe_http_HttpJs("" + client_players_Youtube.playlistUrl + client_players_Youtube.urlVideoId + "&maxResults=50&playlistId=" + client_players_Youtube.extractPlaylistId(url) + "&key=" + client_players_Youtube.apiKey);
+ var id = this.extractPlaylistId(url);
+ var http = new haxe_http_HttpJs("" + this.playlistUrl + this.urlVideoId + "&maxResults=50&playlistId=" + id + "&key=" + this.apiKey);
http.onData = function(data) {
var json = JSON.parse(data);
if(json.error != null) {
@@ -2335,15 +2381,15 @@ client_players_Youtube.prototype = {
var video = window.document.createElement("div");
video.id = "temp-videoplayer";
client_Utils.prepend(this.playerEl,video);
- this.youtube = new YT.Player(video.id,{ videoId : client_players_Youtube.extractVideoId(url), playerVars : { modestbranding : 1, rel : 0, showinfo : 0}, events : { onReady : function(e) {
+ this.tempYoutube = new YT.Player(video.id,{ videoId : this.extractVideoId(url), playerVars : { modestbranding : 1, rel : 0, showinfo : 0}, events : { onReady : function(e) {
if(_gthis.playerEl.contains(video)) {
_gthis.playerEl.removeChild(video);
}
- var tmp = _gthis.youtube.getDuration();
+ var tmp = _gthis.tempYoutube.getDuration();
callback({ duration : tmp});
return;
}, onError : function(e1) {
- haxe_Log.trace("Error " + e1.data,{ fileName : "src/client/players/Youtube.hx", lineNumber : 170, className : "client.players.Youtube", methodName : "getRemoteDataFallback"});
+ haxe_Log.trace("Error " + e1.data,{ fileName : "src/client/players/Youtube.hx", lineNumber : 171, className : "client.players.Youtube", methodName : "getRemoteDataFallback"});
if(_gthis.playerEl.contains(video)) {
_gthis.playerEl.removeChild(video);
}
@@ -2360,11 +2406,18 @@ client_players_Youtube.prototype = {
});
return;
}
+ if(this.youtube != null) {
+ this.youtube.loadVideoById({ videoId : this.extractVideoId(item.url)});
+ return;
+ }
+ this.isLoaded = false;
this.video = window.document.createElement("div");
this.video.id = "videoplayer";
this.playerEl.appendChild(this.video);
- this.youtube = new YT.Player(this.video.id,{ videoId : client_players_Youtube.extractVideoId(item.url), playerVars : { autoplay : 1, modestbranding : 1, rel : 0, showinfo : 0, start : 0}, events : { onReady : function(e) {
- return _gthis.isLoaded = true;
+ this.youtube = new YT.Player(this.video.id,{ videoId : this.extractVideoId(item.url), playerVars : { autoplay : 1, modestbranding : 1, rel : 0, showinfo : 0}, events : { onReady : function(e) {
+ _gthis.isLoaded = true;
+ _gthis.youtube.pauseVideo();
+ return;
}, onStateChange : function(e1) {
switch(e1.data) {
case -1:
@@ -2394,6 +2447,9 @@ client_players_Youtube.prototype = {
if(this.video == null) {
return;
}
+ this.isLoaded = false;
+ this.youtube.destroy();
+ this.youtube = null;
if(this.playerEl.contains(this.video)) {
this.playerEl.removeChild(this.video);
}
@@ -3080,14 +3136,6 @@ Lang.langs = new haxe_ds_StringMap();
Lang.ids = ["en","ru"];
Lang.lang = HxOverrides.substr(window.navigator.language,0,2).toLowerCase();
client_Settings.isSupported = false;
-client_players_Youtube.matchId = new EReg("v=([A-z0-9_-]+)","");
-client_players_Youtube.matchShort = new EReg("youtu.be/([A-z0-9_-]+)","");
-client_players_Youtube.matchEmbed = new EReg("embed/([A-z0-9_-]+)","");
-client_players_Youtube.matchPlaylist = new EReg("youtube\\.com.*list=([A-z0-9_-]+)","");
-client_players_Youtube.videosUrl = "https://www.googleapis.com/youtube/v3/videos";
-client_players_Youtube.playlistUrl = "https://www.googleapis.com/youtube/v3/playlistItems";
-client_players_Youtube.urlTitleDuration = "?part=snippet,contentDetails&fields=items(snippet/title,contentDetails/duration)";
-client_players_Youtube.urlVideoId = "?part=snippet&fields=items(snippet/resourceId/videoId)";
js_youtube_Youtube.isLoadedAPI = false;
client_Main.main();
})(typeof window != "undefined" ? window : typeof global != "undefined" ? global : typeof self != "undefined" ? self : this);
diff --git a/src/client/IPlayer.hx b/src/client/IPlayer.hx
index e414af5..a620567 100644
--- a/src/client/IPlayer.hx
+++ b/src/client/IPlayer.hx
@@ -4,6 +4,7 @@ import Types.VideoData;
import Types.VideoItem;
interface IPlayer {
+ function isSupportedLink(url:String):Bool;
function getVideoData(url:String, callback:(data:VideoData)->Void):Void;
function loadVideo(item:VideoItem):Void;
function removeVideo():Void;
diff --git a/src/client/Player.hx b/src/client/Player.hx
index 8979db8..92cee1c 100644
--- a/src/client/Player.hx
+++ b/src/client/Player.hx
@@ -9,10 +9,14 @@ import client.players.Iframe;
import Types.VideoData;
import Types.VideoItem;
using StringTools;
+using Lambda;
class Player {
final main:Main;
+ final players:Array<IPlayer>;
+ final iframePlayer:IPlayer;
+ final rawPlayer:IPlayer;
final items = new VideoList();
final videoItemsEl = ge("#queue");
final playerEl:Element = ge("#ytapiplayer");
@@ -25,6 +29,11 @@ class Player {
public function new(main:Main):Void {
this.main = main;
+ players = [
+ new Youtube(main, this)
+ ];
+ iframePlayer = new Iframe(main, this);
+ rawPlayer = new Raw(main, this);
initItemButtons();
}
@@ -79,29 +88,27 @@ class Player {
setItemElementType(el, items[pos].isTemp);
}
- function setPlayer(player:IPlayer):Void {
- this.player = player;
+ function setPlayer(newPlayer:IPlayer):Void {
+ if (player != null && player != newPlayer) {
+ player.removeVideo();
+ // playerEl.textContent = "";
+ }
+ player = newPlayer;
}
public function getVideoData(url:String, callback:(data:VideoData)->Void):Void {
- // TODO P2 reuse player objects
- if (Youtube.isYoutube(url)) {
- new Youtube(main, this).getVideoData(url, callback);
- } else {
- new Raw(main, this).getVideoData(url, callback);
- }
+ var player = players.find(player -> player.isSupportedLink(url));
+ if (player == null) player = rawPlayer;
+ player.getVideoData(url, callback);
}
public function setVideo(i:Int):Void {
if (!main.isSyncActive) return;
final item = items[i];
- if (item.isIframe) {
- setPlayer(new Iframe(main, this));
- } else if (Youtube.isYoutube(item.url)) {
- setPlayer(new Youtube(main, this));
- } else {
- setPlayer(new Raw(main, this));
- }
+ var currentPlayer = players.find(p -> p.isSupportedLink(item.url));
+ if (currentPlayer != null) setPlayer(currentPlayer);
+ else if (item.isIframe) setPlayer(iframePlayer);
+ else setPlayer(rawPlayer);
final childs = videoItemsEl.children;
if (childs[itemPos] != null) {
@@ -111,7 +118,6 @@ class Player {
childs[itemPos].classList.add("queue_active");
currentSrc = item.url;
- playerEl.textContent = "";
isLoaded = false;
player.loadVideo(item);
ge("#currenttitle").textContent = item.title;
diff --git a/src/client/players/Iframe.hx b/src/client/players/Iframe.hx
index d79196b..1bb9855 100644
--- a/src/client/players/Iframe.hx
+++ b/src/client/players/Iframe.hx
@@ -20,6 +20,10 @@ class Iframe implements IPlayer {
this.player = player;
}
+ public function isSupportedLink(url:String):Bool {
+ return true;
+ }
+
public function getVideoData(url:String, callback:(data:VideoData)->Void):Void {
callback({
duration: 99 * 60 * 60,
@@ -28,6 +32,7 @@ class Iframe implements IPlayer {
}
public function loadVideo(item:VideoItem):Void {
+ removeVideo();
video = document.createDivElement();
video.id = "videoplayer";
video.innerHTML = item.url;
diff --git a/src/client/players/Raw.hx b/src/client/players/Raw.hx
index c3c96d4..e3c2ed1 100644
--- a/src/client/players/Raw.hx
+++ b/src/client/players/Raw.hx
@@ -10,10 +10,10 @@ import Types.VideoItem;
class Raw implements IPlayer {
- static var controlsHider:Timer;
final main:Main;
final player:Player;
final playerEl:Element = ge("#ytapiplayer");
+ var controlsHider:Timer;
var playAllowed = true;
var video:VideoElement;
@@ -22,6 +22,10 @@ class Raw implements IPlayer {
this.player = player;
}
+ public function isSupportedLink(url:String):Bool {
+ return true;
+ }
+
public function getVideoData(url:String, callback:(data:VideoData)->Void):Void {
var title = url.substr(url.lastIndexOf('/') + 1);
final matchName = ~/^(.+)\./;
@@ -45,18 +49,21 @@ class Raw implements IPlayer {
}
public function loadVideo(item:VideoItem):Void {
+ final url = main.tryLocalIp(item.url);
+ if (video != null) {
+ video.src = url;
+ return;
+ }
video = document.createVideoElement();
video.id = "videoplayer";
- final url = main.tryLocalIp(item.url);
video.src = url;
video.controls = true;
- final isTouch = untyped __js__("'ontouchstart' in window");
if (controlsHider != null) controlsHider.stop();
- if (!isTouch) controlsHider = Timer.delay(() -> {
+ if (!Utils.isTouch()) controlsHider = Timer.delay(() -> {
video.controls = false;
}, 3000);
video.onmousemove = e -> {
- controlsHider.stop();
+ if (controlsHider != null) controlsHider.stop();
video.controls = true;
video.onmousemove = null;
}
diff --git a/src/client/players/Youtube.hx b/src/client/players/Youtube.hx
index cc4269d..3ea0e72 100644
--- a/src/client/players/Youtube.hx
+++ b/src/client/players/Youtube.hx
@@ -13,33 +13,33 @@ using StringTools;
class Youtube implements IPlayer {
- static final matchId = ~/v=([A-z0-9_-]+)/;
- static final matchShort = ~/youtu.be\/([A-z0-9_-]+)/;
- static final matchEmbed = ~/embed\/([A-z0-9_-]+)/;
- static final matchPlaylist = ~/youtube\.com.*list=([A-z0-9_-]+)/;
- static final videosUrl = "https://www.googleapis.com/youtube/v3/videos";
- static final playlistUrl = "https://www.googleapis.com/youtube/v3/playlistItems";
- static final urlTitleDuration = "?part=snippet,contentDetails&fields=items(snippet/title,contentDetails/duration)";
- static final urlVideoId = "?part=snippet&fields=items(snippet/resourceId/videoId)";
- static var apiKey:String;
+ final matchId = ~/v=([A-z0-9_-]+)/;
+ final matchShort = ~/youtu.be\/([A-z0-9_-]+)/;
+ final matchEmbed = ~/embed\/([A-z0-9_-]+)/;
+ final matchPlaylist = ~/youtube\.com.*list=([A-z0-9_-]+)/;
+ final videosUrl = "https://www.googleapis.com/youtube/v3/videos";
+ final playlistUrl = "https://www.googleapis.com/youtube/v3/playlistItems";
+ final urlTitleDuration = "?part=snippet,contentDetails&fields=items(snippet/title,contentDetails/duration)";
+ final urlVideoId = "?part=snippet&fields=items(snippet/resourceId/videoId)";
final main:Main;
final player:Player;
final playerEl:Element = ge("#ytapiplayer");
+ var apiKey:String;
var video:Element;
var youtube:YoutubePlayer;
+ var tempYoutube:YoutubePlayer;
var isLoaded = false;
public function new(main:Main, player:Player) {
this.main = main;
this.player = player;
- apiKey = main.getYoutubeApiKey();
}
- public static function isYoutube(url:String):Bool {
+ public function isSupportedLink(url:String):Bool {
return extractVideoId(url) != "" || extractPlaylistId(url) != "";
}
- static function extractVideoId(url:String):String {
+ function extractVideoId(url:String):String {
if (url.contains("youtu.be/")) {
matchShort.match(url);
return matchShort.matched(1);
@@ -52,7 +52,7 @@ class Youtube implements IPlayer {
return matchId.matched(1);
}
- static function extractPlaylistId(url:String):String {
+ function extractPlaylistId(url:String):String {
if (!matchPlaylist.match(url)) return "";
return matchPlaylist.matched(1);
}
@@ -73,6 +73,7 @@ class Youtube implements IPlayer {
}
public function getVideoData(url:String, callback:(data:VideoData)->Void):Void {
+ if (apiKey == null) apiKey = main.getYoutubeApiKey();
var id = extractVideoId(url);
if (id == "") {
getPlaylistVideoData(url, callback);
@@ -151,7 +152,7 @@ class Youtube implements IPlayer {
final video = document.createDivElement();
video.id = "temp-videoplayer";
Utils.prepend(playerEl, video);
- youtube = new YoutubePlayer(video.id, {
+ tempYoutube = new YoutubePlayer(video.id, {
videoId: extractVideoId(url),
playerVars: {
modestbranding: 1,
@@ -162,7 +163,7 @@ class Youtube implements IPlayer {
onReady: e -> {
if (playerEl.contains(video)) playerEl.removeChild(video);
callback({
- duration: youtube.getDuration()
+ duration: tempYoutube.getDuration()
});
},
onError: e -> {
@@ -180,6 +181,13 @@ class Youtube implements IPlayer {
YtInit.init(() -> loadVideo(item));
return;
}
+ if (youtube != null) {
+ youtube.loadVideoById({
+ videoId: extractVideoId(item.url)
+ });
+ return;
+ }
+ isLoaded = false;
video = document.createDivElement();
video.id = "videoplayer";
playerEl.appendChild(video);
@@ -190,11 +198,13 @@ class Youtube implements IPlayer {
autoplay: 1,
modestbranding: 1,
rel: 0,
- showinfo: 0,
- start: 0
+ showinfo: 0
},
events: {
- onReady: e -> isLoaded = true,
+ onReady: e -> {
+ isLoaded = true;
+ youtube.pauseVideo();
+ },
onStateChange: e -> {
switch (e.data) {
case UNSTARTED:
@@ -211,13 +221,16 @@ class Youtube implements IPlayer {
},
onPlaybackRateChange: e -> {
player.onRateChange();
- },
+ }
}
});
}
public function removeVideo():Void {
if (video == null) return;
+ isLoaded = false;
+ youtube.destroy();
+ youtube = null;
if (playerEl.contains(video)) playerEl.removeChild(video);
video = null;
}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage