Add files via upload

ff_linkkf
This commit is contained in:
2026-04-01 11:04:15 +09:00
committed by GitHub
commit 704d105bb0
29 changed files with 4787 additions and 0 deletions
+243
View File
@@ -0,0 +1,243 @@
button.code-button {
min-width: 82px !important;
}
.tooltip {
position: relative;
display: block;
}
[data-tooltip-text]:hover {
position: relative;
}
[data-tooltip-text]:after {
-webkit-transition: bottom 0.3s ease-in-out, opacity 0.3s ease-in-out;
-moz-transition: bottom 0.3s ease-in-out, opacity 0.3s ease-in-out;
transition: bottom 0.3s ease-in-out, opacity 0.3s ease-in-out;
background-color: rgba(0, 0, 0, 0.8);
-webkit-box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);
-moz-box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);
box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
color: #ffffff;
font-size: 12px;
margin-bottom: 10px;
padding: 7px 12px;
position: absolute;
width: auto;
min-width: 50px;
max-width: 300px;
word-wrap: break-word;
z-index: 9999;
opacity: 0;
left: -9999px;
top: 90%;
content: attr(data-tooltip-text);
}
[data-tooltip-text]:hover:after {
top: 230%;
left: 0;
opacity: 1;
}
[data-tooltip-text]:hover {
position: relative;
}
[data-tooltip-text]:after {
-webkit-transition: bottom 0.3s ease-in-out, opacity 0.3s ease-in-out;
-moz-transition: bottom 0.3s ease-in-out, opacity 0.3s ease-in-out;
transition: bottom 0.3s ease-in-out, opacity 0.3s ease-in-out;
background-color: rgba(0, 0, 0, 0.8);
-webkit-box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);
-moz-box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);
box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
color: #ffffff;
font-size: 12px;
margin-bottom: 10px;
padding: 7px 12px;
position: absolute;
width: auto;
min-width: 50px;
max-width: 300px;
word-wrap: break-word;
z-index: 9999;
opacity: 0;
left: -9999px;
top: -210% !important;
content: attr(data-tooltip-text);
}
[data-tooltip-text]:hover:after {
top: 130%;
left: 0;
opacity: 1;
}
#airing_list {
display: none;
}
.cut-text {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
}
@media (min-width: 576px) {
.container {
max-width: 100%;
}
}
@media (min-width: 1280px) {
#linkkf_wrapper {
max-width: 80%;
margin: 0 auto;
}
}
#screen_movie_list {
margin-top: 10px;
}
/* .spinner {*/
/* width: 40px;*/
/* height: 40px;*/
/* background-color: #333;*/
/* margin: 100px auto;*/
/* -webkit-animation: sk-rotateplane 1.2s infinite ease-in-out;*/
/* animation: sk-rotateplane 1.2s infinite ease-in-out;*/
/*}*/
/*@-webkit-keyframes sk-rotateplane {*/
/* 0% { -webkit-transform: perspective(120px) }*/
/* 50% { -webkit-transform: perspective(120px) rotateY(180deg) }*/
/* 100% { -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg) }*/
/*}*/
/*@keyframes sk-rotateplane {*/
/* 0% {*/
/* transform: perspective(120px) rotateX(0deg) rotateY(0deg);*/
/* -webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg)*/
/* } 50% {*/
/* transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);*/
/* -webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg)*/
/* } 100% {*/
/* transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);*/
/* -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);*/
/* }*/
/*}*/
.spinner {
width: 40px;
height: 40px;
position: relative;
margin: 100px auto;
}
.double-bounce1,
.double-bounce2 {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #333;
opacity: 0.6;
position: absolute;
top: 0;
left: 0;
-webkit-animation: sk-bounce 2s infinite ease-in-out;
animation: sk-bounce 2s infinite ease-in-out;
}
.double-bounce2 {
-webkit-animation-delay: -1s;
animation-delay: -1s;
}
@-webkit-keyframes sk-bounce {
0%,
100% {
-webkit-transform: scale(0);
}
50% {
-webkit-transform: scale(1);
}
}
@keyframes sk-bounce {
0%,
100% {
transform: scale(0);
-webkit-transform: scale(0);
}
50% {
transform: scale(1);
-webkit-transform: scale(1);
}
}
.badge-on-image {
position: absolute;
top: 2px;
/*bottom: 2px; !* position where you want it *!*/
right: 2px;
padding: 5px 12px;
}
#inner_screen_movie > div {
margin-bottom: 10px;
}
.card-body {
padding: 0!important;
}
.new-anime {
border-color: darksalmon;
border-width: 4px;
border-style: dashed;
}
.card-title {
padding: 1rem!important;
}
button#add_whitelist {
float: right;
}
button.btn-favorite {
background-color: #e0ff42;
}
body {
font-family: NanumSquareNeo,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Noto Sans,Liberation Sans,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
}
body {
background-image: linear-gradient(90deg, #233f48, #6c6fa2, #768dae);
}
+9
View File
@@ -0,0 +1,9 @@
#list_div img {
max-width: 100%;
height: auto;
}
#page1,
#page2 {
margin: 12px 0;
}
+162
View File
@@ -0,0 +1,162 @@
button.code-button {
min-width: 82px !important;
}
.tooltip {
position: relative;
display: block;
}
[data-tooltip-text]:hover {
position: relative;
}
[data-tooltip-text]:after {
-webkit-transition: bottom 0.3s ease-in-out, opacity 0.3s ease-in-out;
-moz-transition: bottom 0.3s ease-in-out, opacity 0.3s ease-in-out;
transition: bottom 0.3s ease-in-out, opacity 0.3s ease-in-out;
background-color: rgba(0, 0, 0, 0.8);
-webkit-box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);
-moz-box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);
box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
color: #ffffff;
font-size: 12px;
margin-bottom: 10px;
padding: 7px 12px;
position: absolute;
width: auto;
min-width: 50px;
max-width: 300px;
word-wrap: break-word;
z-index: 9999;
opacity: 0;
left: -9999px;
top: 90%;
content: attr(data-tooltip-text);
}
[data-tooltip-text]:hover:after {
top: 230%;
left: 0;
opacity: 1;
}
[data-tooltip-text]:hover {
position: relative;
}
[data-tooltip-text]:after {
-webkit-transition: bottom 0.3s ease-in-out, opacity 0.3s ease-in-out;
-moz-transition: bottom 0.3s ease-in-out, opacity 0.3s ease-in-out;
transition: bottom 0.3s ease-in-out, opacity 0.3s ease-in-out;
background-color: rgba(0, 0, 0, 0.8);
-webkit-box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);
-moz-box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);
box-shadow: 0px 0px 3px 1px rgba(50, 50, 50, 0.4);
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
color: #ffffff;
font-size: 12px;
margin-bottom: 10px;
padding: 7px 12px;
position: absolute;
width: auto;
min-width: 50px;
max-width: 300px;
word-wrap: break-word;
z-index: 9999;
opacity: 0;
left: -9999px;
top: -210% !important;
content: attr(data-tooltip-text);
}
[data-tooltip-text]:hover:after {
top: 130%;
left: 0;
opacity: 1;
}
#airing_list {
display: none;
}
.card {
border: none;
box-shadow: inset 1px 1px hsl(0deg 0% 100% / 20%), inset -1px -1px hsl(0deg 0% 100% / 10%), 1px 3px 24px -1px rgb(0 0 0 / 15%);
background-color: transparent;
background-image: linear-gradient(125deg, hsla(0, 0%, 100%, .3), hsla(0, 0%, 100%, .2) 70%);
backdrop-filter: blur(5px);
}
.card.border-light {
--bs-border-opacity: 1;
border-color: rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important;
}
#request {
color: aliceblue;
}
body {
font-family: NanumSquareNeo, system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Noto Sans, Liberation Sans, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
}
body {
background-image: linear-gradient(90deg, #33242c, #263341, #17273a);
}
@media (min-width: 1280px) {
#request {
max-width: 80%;
margin: 0 auto;
}
}
@media (min-width: 992px) {
.container {
max-width: 96%;
}
}
@media (min-width: 768px) {
.container {
max-width: 94%;
}
}
@media (min-width: 576px) {
.container {
max-width: 96%;
}
.form-inline .form-control {
width: 98%;
}
}
#preloader,
.loader-inner,
.loader-line-wrap,
.loader-line {
display: none !important;
}
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: rgb(241, 242, 243); display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<g transform="translate(27.166666666666664,27.166666666666664)">
<rect x="-18.5" y="-18.5" width="37" height="37" fill="#85a2b6">
<animateTransform attributeName="transform" type="scale" repeatCount="indefinite" dur="1s" keyTimes="0;1" values="1.1;1" begin="-0.3s"></animateTransform>
</rect>
</g>
<g transform="translate(72.83333333333333,27.166666666666664)">
<rect x="-18.5" y="-18.5" width="37" height="37" fill="#bbcedd">
<animateTransform attributeName="transform" type="scale" repeatCount="indefinite" dur="1s" keyTimes="0;1" values="1.1;1" begin="-0.2s"></animateTransform>
</rect>
</g>
<g transform="translate(27.166666666666664,72.83333333333333)">
<rect x="-18.5" y="-18.5" width="37" height="37" fill="#dce4eb">
<animateTransform attributeName="transform" type="scale" repeatCount="indefinite" dur="1s" keyTimes="0;1" values="1.1;1" begin="0s"></animateTransform>
</rect>
</g>
<g transform="translate(72.83333333333333,72.83333333333333)">
<rect x="-18.5" y="-18.5" width="37" height="37" fill="#fdfdfd">
<animateTransform attributeName="transform" type="scale" repeatCount="indefinite" dur="1s" keyTimes="0;1" values="1.1;1" begin="-0.1s"></animateTransform>
</rect>
</g>
<!-- [ldio] generated by https://loading.io/ --></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+297
View File
@@ -0,0 +1,297 @@
let currentCate = "ing";
let nextPage = 2;
let totalPage = 1;
let isLoading = false;
const spinner = document.getElementById("spinner");
const listContainer = document.getElementById("screen_movie_list");
const categoryButtons = document.querySelectorAll("#anime_category button");
const categoryConfig = {
ing: {
endpoint: "anime_list",
makeData: (page) => ({ page: String(page), type: "ing" }),
title: "애니",
},
movie: {
endpoint: "screen_movie_list",
makeData: (page) => ({ page: String(page) }),
title: "극장판",
},
complete: {
endpoint: "complete_anilist",
makeData: (page) => ({ page: String(page) }),
title: "성인",
},
top_view: {
endpoint: "anime_list",
makeData: (page) => ({ page: String(page), type: "top_view" }),
title: "최신",
},
};
function setLoading(flag) {
isLoading = flag;
spinner.style.display = flag ? "block" : "none";
}
function setActiveCategory(cate) {
categoryButtons.forEach((button) => {
button.classList.toggle("active", button.id === cate);
});
}
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function getImageUrl(imageLink) {
const image = String(imageLink || "").trim();
return image === "" ? placeholder_image : image;
}
function renderEmpty(message) {
listContainer.innerHTML = `<div class="alert alert-secondary mt-2">${escapeHtml(message)}</div>`;
}
function openPlayerPage(url) {
if (!url) {
$.notify("<strong>재생 URL을 찾지 못했습니다.</strong>", {
type: "warning",
});
return;
}
window.open(url, "_blank");
}
async function playLatest(code, title) {
try {
const response = await fetch(`/${package_name}/ajax/play_latest`, {
method: "POST",
cache: "no-cache",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
code: String(code || ""),
title: String(title || ""),
}),
});
const ret = await response.json();
if (ret.ret !== "success" || ret.data == null) {
$.notify(`<strong>${escapeHtml(ret.log || "재생 정보를 가져오지 못했습니다.")}</strong>`, {
type: "warning",
});
return;
}
openPlayerPage(ret.data.play_url);
} catch (error) {
console.error(error);
$.notify("<strong>재생 정보를 가져오지 못했습니다.</strong>", {
type: "warning",
});
}
}
function renderItems(ret, append = false) {
const items = Array.isArray(ret.episode) ? ret.episode : [];
const page = Number(ret.page || 1);
const title = categoryConfig[currentCate]?.title || "목록";
if (items.length === 0 && append === false) {
renderEmpty(`${title} 목록이 없습니다.`);
return;
}
let html = "";
if (append === false) {
html += `<div id="page_caption" style="padding-bottom: 3px">`;
html += `<button type="button" class="btn btn-info">${escapeHtml(title)}</button>`;
html += `</div>`;
html += `<div id="inner_screen_movie" class="row infinite-scroll">`;
}
for (const item of items) {
const code = escapeHtml(item.code);
const titleText = escapeHtml(item.title);
const chapter = escapeHtml(item.chapter || "");
const imageUrl = escapeHtml(getImageUrl(item.image_link));
html += `<div class="col-6 col-sm-4 col-md-3 mb-3">`;
html += `<div class="card h-100">`;
html += `<img class="card-img-top" src="${imageUrl}" alt="${titleText}" loading="lazy" style="cursor: pointer" onclick="location.href='${request_path}?code=${code}'" />`;
if (chapter !== "") {
html += `<span class="badge badge-danger badge-on-image">${chapter}</span>`;
}
html += `<div class="card-body d-flex flex-column">`;
html += `<h5 class="card-title">${titleText}</h5>`;
html += `<a href="${request_path}?code=${code}" class="btn btn-primary btn-sm mb-2">분석</a>`;
html += `<button type="button" class="btn btn-outline-info btn-sm play-latest-btn" data-code="${code}" data-title="${titleText}">최신화 보기</button>`;
html += `</div>`;
html += `</div>`;
html += `</div>`;
}
if (append === false) {
html += `</div>`;
listContainer.innerHTML = html;
} else {
const wrapper = document.createElement("div");
wrapper.innerHTML = html;
const target = document.getElementById("inner_screen_movie");
while (wrapper.firstChild) {
target.appendChild(wrapper.firstChild);
}
}
if (page >= totalPage) {
nextPage = totalPage + 1;
} else {
nextPage = page + 1;
}
}
async function fetchCategory(cate, page = 1, append = false) {
const config = categoryConfig[cate];
if (config === undefined || isLoading) {
return;
}
currentCate = cate;
setActiveCategory(cate);
setLoading(true);
try {
const response = await fetch(`/${package_name}/ajax/${config.endpoint}`, {
method: "POST",
cache: "no-cache",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams(config.makeData(page)),
});
const ret = await response.json();
if (ret.ret !== "success") {
renderEmpty(ret.log || "목록을 불러오지 못했습니다.");
return;
}
totalPage = Number(ret.total_page || 1);
renderItems(ret, append);
} catch (error) {
console.error(error);
renderEmpty("목록을 불러오지 못했습니다.");
} finally {
setLoading(false);
}
}
async function runSearch() {
const input = document.getElementById("input_search");
const query = input.value.trim();
if (query === "" || isLoading) {
return;
}
setLoading(true);
try {
const response = await fetch(`/${package_name}/ajax/search`, {
method: "POST",
cache: "no-cache",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({ query }),
});
const ret = await response.json();
if (ret.ret !== "success") {
renderEmpty(ret.log || "검색 결과를 불러오지 못했습니다.");
return;
}
currentCate = "search";
setActiveCategory("");
totalPage = 1;
nextPage = 2;
const result = {
page: 1,
total_page: 1,
episode: Array.isArray(ret.episode) ? ret.episode : [],
};
renderItems(result, false);
if (result.episode.length === 0) {
renderEmpty(`"${query}" 검색 결과가 없습니다.`);
}
} catch (error) {
console.error(error);
renderEmpty("검색 결과를 불러오지 못했습니다.");
} finally {
setLoading(false);
}
}
function loadNextPage() {
if (isLoading) {
return;
}
if (currentCate === "search" || currentCate === "top_view") {
return;
}
if (nextPage > totalPage) {
return;
}
fetchCategory(currentCate, nextPage, true);
}
function onCategoryClick(event) {
const cate = event.target.id;
if (categoryConfig[cate] === undefined) {
return;
}
nextPage = 2;
totalPage = 1;
fetchCategory(cate, 1, false);
}
function onScroll() {
const threshold = 50;
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
if (clientHeight + scrollTop + threshold >= scrollHeight) {
loadNextPage();
}
}
function debounce(func, delay) {
let timeoutId = null;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
}
document.getElementById("anime_category").addEventListener("click", onCategoryClick);
document.getElementById("btn_search").addEventListener("click", runSearch);
document.getElementById("input_search").addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
runSearch();
}
});
document.addEventListener("scroll", debounce(onScroll, 250));
document.body.addEventListener("click", (event) => {
const button = event.target.closest(".play-latest-btn");
if (!button) {
return;
}
event.preventDefault();
playLatest(button.dataset.code, button.dataset.title);
});
document.addEventListener("DOMContentLoaded", () => {
fetchCategory("ing", 1, false);
});
+249
View File
@@ -0,0 +1,249 @@
(function () {
if (window.__linkkfListInitialized === true) {
return;
}
window.__linkkfListInitialized = true;
let currentData = null;
const escapeHtml = (value) =>
String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
function getFormData() {
return $("#form_search").serialize();
}
function normalizeObject(value) {
if (value && typeof value === "object") {
return value;
}
if (typeof value === "string" && value.trim() !== "") {
try {
return JSON.parse(value);
} catch (e) {
return {};
}
}
return {};
}
function openJsonModal(item) {
$("#modal_title").text("JSON");
$("#modal_body").html(`<pre style="white-space: pre-wrap; word-break: break-all;">${escapeHtml(JSON.stringify(item, null, 2))}</pre>`);
$("#large_modal").modal("show");
}
function makeActionButton(id, label, attrs = {}, klass = "btn btn-sm btn-primary mr-1 mb-1") {
const extra = Object.entries(attrs)
.map(([key, value]) => ` data-${key}="${escapeHtml(value)}"`)
.join("");
return `<button id="${escapeHtml(id)}" class="${klass}"${extra}>${escapeHtml(label)}</button>`;
}
function getStatusLabel(item) {
if (item.status === "completed") {
return "완료";
}
if (item.status === "error") {
return "실패";
}
if (item.status === "canceled") {
return "취소";
}
if (item.status === "downloading") {
return "진행중";
}
return "대기";
}
function renderPagination(paging) {
if (!paging) {
$("#page1").html("");
$("#page2").html("");
return;
}
let html = `
<div class="row mb-3">
<div class="col-sm-12">
<div class="btn-toolbar justify-content-center" role="toolbar">
<div class="btn-group btn-group-sm mr-2" role="group">
`;
if (paging.prev_page) {
html += `<button id="page" data-page="${paging.start_page - 1}" type="button" class="btn btn-secondary">&laquo;</button>`;
}
for (let i = paging.start_page; i <= paging.last_page; i++) {
const disabled = i === paging.current_page ? " disabled" : "";
html += `<button id="page" data-page="${i}" type="button" class="btn btn-secondary"${disabled}>${i}</button>`;
}
if (paging.next_page) {
html += `<button id="page" data-page="${paging.last_page + 1}" type="button" class="btn btn-secondary">&raquo;</button>`;
}
html += `
</div>
</div>
</div>
</div>
`;
$("#page1").html(html);
$("#page2").html(html);
}
function renderList(list) {
if (!Array.isArray(list) || list.length === 0) {
$("#list_div").html('<div class="alert alert-secondary">목록이 없습니다.</div>');
return;
}
let html = "";
for (const item of list) {
const statusLabel = getStatusLabel(item);
const contents = normalizeObject(item.contents_json || item.linkkf_info);
const savePath = item.save_path || contents.save_path || "";
const filename = item.filename || contents.filename || "";
const programCode = item.programcode || contents.program_code || "";
const programTitle = contents.program_title || contents.save_folder || contents.title || filename;
const completedTime = item.completed_time ? `<div>${escapeHtml(item.completed_time)} (${escapeHtml(statusLabel)})</div>` : "";
let actions = "";
actions += makeActionButton("json_btn", "JSON", { id: item.id }, "btn btn-sm btn-info mr-1 mb-1");
actions += makeActionButton("request_btn", "작품 검색", { content_code: programCode }, "btn btn-sm btn-primary mr-1 mb-1");
actions += makeActionButton("self_search_btn", "목록 검색", { title: programTitle }, "btn btn-sm btn-secondary mr-1 mb-1");
actions += makeActionButton("remove_btn", "삭제", { id: item.id }, "btn btn-sm btn-danger mr-1 mb-1");
html += `
<div class="card mb-3">
<div class="card-body">
<div class="row">
<div class="col-md-1"><strong>${escapeHtml(item.id)}</strong></div>
<div class="col-md-2">${escapeHtml(statusLabel)}</div>
<div class="col-md-3">
<div>${escapeHtml(item.created_time)} (추가)</div>
${completedTime}
</div>
<div class="col-md-6">
<div>${escapeHtml(savePath)}</div>
<div>${escapeHtml(filename)}</div>
<div class="mt-2">${actions}</div>
</div>
</div>
</div>
</div>
`;
}
$("#list_div").html(html);
}
function loadList(page, moveTop = true) {
let formData = getFormData();
formData += "&page=" + page;
$.ajax({
url: "/" + package_name + "/ajax/web_list",
type: "POST",
cache: false,
data: formData,
dataType: "json",
success: (data) => {
currentData = data;
if (data && !data.ret) {
if (moveTop) {
window.scrollTo(0, 0);
}
renderList(data.list);
renderPagination(data.paging);
} else {
$.notify("<strong>목록을 불러오지 못했습니다.</strong>", {
type: "warning",
});
}
},
error: () => {
$.notify("<strong>목록 요청 중 오류가 발생했습니다.</strong>", {
type: "warning",
});
},
});
}
$(document).ready(function () {
loadList(1);
});
$("#search").click(function (e) {
e.preventDefault();
loadList(1);
});
$("#reset_btn").click(function (e) {
e.preventDefault();
document.getElementById("form_search").reset();
loadList(1);
});
$("body").on("click", "#page", function (e) {
e.preventDefault();
loadList($(this).data("page"));
});
$("body").on("click", "#remove_btn", function (e) {
e.preventDefault();
const id = $(this).data("id");
$.ajax({
url: "/" + package_name + "/ajax/db_remove",
type: "POST",
cache: false,
data: { id },
dataType: "json",
success: function (ret) {
if (ret) {
$.notify("<strong>삭제했습니다.</strong>", {
type: "success",
});
loadList(currentData?.paging?.current_page || 1, false);
} else {
$.notify("<strong>삭제 실패</strong>", {
type: "warning",
});
}
},
});
});
$("body").on("click", "#json_btn", function (e) {
e.preventDefault();
const id = $(this).data("id");
const target = currentData?.list?.find((item) => String(item.id) === String(id));
if (target) {
openJsonModal(target);
}
});
$("body").on("click", "#self_search_btn", function (e) {
e.preventDefault();
document.getElementById("search_word").value = $(this).data("title");
loadList(1);
});
$("body").on("click", "#request_btn", function (e) {
e.preventDefault();
const contentCode = $(this).data("content_code");
if (!contentCode) {
$.notify("<strong>작품 코드를 찾지 못했습니다.</strong>", {
type: "warning",
});
return;
}
window.location.href = "/" + package_name + "/request?code=" + contentCode;
});
})();
+477
View File
@@ -0,0 +1,477 @@
(function () {
if (window.__linkkfRequestInitialized === true) {
return;
}
window.__linkkfRequestInitialized = true;
let currentData = null;
let currentAiringData = null;
let code = "";
let analysisInProgress = false;
const normalizeCode = (value) => {
const raw = String(value || "").trim();
if (raw === "") {
return "";
}
const match = raw.match(/\/(?:ani|watch)\/(\d+)\//) || raw.match(/(\d{3,})/);
return match ? match[1] : raw;
};
const escapeHtml = (value) =>
String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
const mButton = (id, text, attrs = [], extraClass = "btn-primary") => {
const extraAttrs = (attrs || [])
.map((item) => ` data-${item.key}="${escapeHtml(item.value)}"`)
.join("");
return `<button id="${escapeHtml(id)}" class="btn btn-sm ${extraClass} mr-1 mb-1"${extraAttrs}>${escapeHtml(text)}</button>`;
};
const mButtonGroup = (inner) =>
`<div class="d-flex flex-wrap align-items-center mb-3">${inner}</div>`;
const mHrBlack = () => '<hr class="my-3" style="border-color: rgba(255,255,255,0.25);">';
const mHr = () => '<hr class="my-2" style="border-color: rgba(255,255,255,0.15);">';
const mRowStart = () => '<div class="row">';
const mRowEnd = () => "</div>";
const mCol = (size, content, align) => {
const alignClass = align === "right" ? " text-right" : "";
return `<div class="col-md-${size}${alignClass}">${content ?? ""}</div>`;
};
function notifyWarning(message) {
$.notify(`<strong>${message}</strong>`, { type: "warning" });
}
function notifySuccess(message) {
$.notify(`<strong>${message}</strong>`, { type: "success" });
}
function setAnalysisLoading(flag) {
analysisInProgress = flag === true;
const analysisButton = document.getElementById("analysis_btn");
if (analysisButton) {
analysisButton.disabled = analysisInProgress;
}
}
function openPlayerPage(url) {
if (!url) {
notifyWarning("재생 URL을 가져오지 못했습니다.");
return;
}
window.open(url, "_blank");
}
function requestPlay(payload) {
$.ajax({
url: `/${package_name}/ajax/play`,
type: "POST",
cache: false,
data: payload,
dataType: "json",
success: function (ret) {
if (ret.ret === "success" && ret.data != null) {
openPlayerPage(ret.data.play_url);
} else {
notifyWarning(ret.log || "재생 정보를 가져오지 못했습니다.");
}
},
error: function () {
notifyWarning("재생 정보를 가져오지 못했습니다.");
},
});
}
function getAiringList() {
$.ajax({
url: `/${package_name}/ajax/airing_list`,
type: "GET",
cache: false,
dataType: "json",
success: (ret) => {
if (ret.ret === "success" && Array.isArray(ret.episode)) {
currentAiringData = ret;
makeAiringList(ret);
} else {
notifyWarning(ret.log || "최신 목록을 불러오지 못했습니다.");
}
},
error: function () {
notifyWarning("최신 목록을 불러오지 못했습니다.");
},
});
}
function makeAiringList(data) {
let str = "";
str += mHrBlack();
str += '<div id="inner_airing" class="d-flex flex-wrap">';
for (const item of data.episode) {
str += `
<div class="mx-1 mb-1">
<button
type="button"
class="btn btn-primary code-button"
data-code="${escapeHtml(item.code)}"
title="${escapeHtml(item.title)}"
>
${escapeHtml(item.code)}
</button>
</div>
`;
}
str += "</div>";
str += mHrBlack();
document.getElementById("airing_list").innerHTML = str;
}
function renderProgram(data) {
currentData = data;
let str = "";
let tmp = '<div class="form-inline w-100">';
tmp += mButton("check_download_btn", "선택 다운로드 추가");
tmp += mButton("all_check_on_btn", "전체 선택");
tmp += mButton("all_check_off_btn", "전체 해제");
tmp += mButton("down_subtitle_btn", "자막 전체 받기");
tmp += `&nbsp;&nbsp;&nbsp;<input id="new_title" name="new_title" class="form-control form-control-sm" value="${escapeHtml(data.title)}">`;
tmp += "</div>";
tmp += '<div class="form-inline">';
tmp += mButton("apply_new_title_btn", "폴더명 변경");
tmp += `&nbsp;&nbsp;&nbsp;<input id="new_season" name="new_season" class="form-control form-control-sm" value="${escapeHtml(data.season)}">`;
tmp += mButton("apply_new_season_btn", "시즌 변경");
tmp += mButton("search_tvdb_btn", "TVDB", [], "btn-outline-info");
tmp += mButton("add_whitelist", "자동 다운로드 추가", [], "btn-outline-success");
tmp += "</div>";
str += mButtonGroup(tmp);
str += "<div class='card p-lg-5 mt-md-3 p-md-3 border-light'>";
str += mRowStart();
str += mCol(3, data.poster_url ? `<img src="${escapeHtml(data.poster_url)}" class="img-fluid">` : "");
let detailHtml = "";
detailHtml += mRowStart();
detailHtml += mCol(3, "제목", "right");
detailHtml += mCol(9, escapeHtml(data.title));
detailHtml += mRowEnd();
detailHtml += mRowStart();
detailHtml += mCol(3, "시즌", "right");
detailHtml += mCol(9, escapeHtml(data.season));
detailHtml += mRowEnd();
for (const detailRow of data.detail || []) {
const key = Object.keys(detailRow)[0];
const value = detailRow[key];
detailHtml += mRowStart();
detailHtml += mCol(3, escapeHtml(key), "right");
detailHtml += mCol(9, escapeHtml(value));
detailHtml += mRowEnd();
}
str += mCol(9, detailHtml);
str += mRowEnd();
str += "</div>";
for (let i = 0; i < (data.episode || []).length; i += 1) {
const episode = data.episode[i];
str += mRowStart();
tmp = `<strong>${escapeHtml(episode.title)}</strong><br>`;
tmp += `${escapeHtml(episode.filename)}<br><p></p>`;
tmp += '<div class="form-inline">';
tmp += `
<input
id="checkbox_${escapeHtml(episode.code)}"
name="checkbox_${escapeHtml(episode.code)}"
type="checkbox"
checked
data-toggle="toggle"
data-on="선택"
data-off="-"
data-onstyle="success"
data-offstyle="danger"
data-size="small"
>&nbsp;&nbsp;&nbsp;&nbsp;
`;
tmp += mButton("add_queue_btn", "다운로드 추가", [{ key: "idx", value: i }]);
tmp += mButton("play_video_btn", "보기", [{ key: "idx", value: i }], "btn-outline-info");
tmp += "</div>";
str += mCol(12, tmp);
str += mRowEnd();
if (i !== data.episode.length - 1) {
str += mHr();
}
}
document.getElementById("episode_list").innerHTML = str;
$('input[id^="checkbox_"]').bootstrapToggle();
}
$("body").on("click", "button.code-button", function (e) {
e.preventDefault();
document.getElementById("code").value = $(this).data("code");
$("#airing_list").toggle();
runAnalysis();
});
function runAnalysis() {
if (analysisInProgress) {
return;
}
const input = document.getElementById("code");
code = normalizeCode(input.value);
input.value = code;
if (code === "") {
notifyWarning("code 값을 입력해 주세요.");
return;
}
setAnalysisLoading(true);
$.ajax({
url: `/${package_name}/ajax/analysis`,
type: "POST",
cache: false,
data: { code },
dataType: "json",
success: function (ret) {
if (ret.ret === "success" && ret.data != null) {
renderProgram(ret.data);
} else {
notifyWarning(ret.log || "분석에 실패했습니다.");
}
},
error: function () {
notifyWarning("분석 요청에 실패했습니다.");
},
complete: function () {
setAnalysisLoading(false);
},
});
}
$("body").on("click", "#analysis_btn", function (e) {
e.preventDefault();
runAnalysis();
});
$("body").on("click", "#go_linkkf_btn", function (e) {
e.preventDefault();
window.open(linkkf_url, "_blank");
});
$("body").on("click", "#all_check_on_btn", function (e) {
e.preventDefault();
$('input[id^="checkbox_"]').bootstrapToggle("on");
});
$("body").on("click", "#all_check_off_btn", function (e) {
e.preventDefault();
$('input[id^="checkbox_"]').bootstrapToggle("off");
});
$("body").on("click", "#search_tvdb_btn", function (e) {
e.preventDefault();
const newTitle = document.getElementById("new_title").value;
window.open(`https://www.thetvdb.com/search?query=${encodeURIComponent(newTitle)}`, "_blank");
});
$("body").on("click", "#add_whitelist", function (e) {
e.preventDefault();
$.ajax({
url: `/${package_name}/ajax/add_whitelist`,
type: "POST",
cache: false,
dataType: "json",
success: function (ret) {
if (ret.ret) {
notifySuccess("추가되었습니다.");
renderProgram(ret);
} else {
notifyWarning(ret.log || "추가에 실패했습니다.");
}
},
error: function () {
notifyWarning("추가에 실패했습니다.");
},
});
});
$("body").on("click", "#down_subtitle_btn", function (e) {
e.preventDefault();
const all = $('input[id^="checkbox_"]');
let str = "";
for (let i = 0; i < all.length; i += 1) {
if (all[i].checked) {
str += `${all[i].id.split("_")[1]},`;
}
}
if (str === "") {
notifyWarning("선택해 주세요.");
return;
}
$.ajax({
url: `/${package_name}/ajax/down_subtitle_list`,
type: "POST",
cache: false,
data: { code: str },
dataType: "json",
success: function (ret) {
if (ret.ret === "success") {
notifySuccess(`${ret.log}개를 처리했습니다.`);
} else {
notifyWarning(ret.log || "자막 다운로드에 실패했습니다.");
}
},
});
});
$("body").on("click", "#apply_new_title_btn", function (e) {
e.preventDefault();
const newTitle = document.getElementById("new_title").value;
$.ajax({
url: `/${package_name}/ajax/apply_new_title`,
type: "POST",
cache: false,
data: { new_title: newTitle },
dataType: "json",
success: function (ret) {
if (ret.ret) {
notifySuccess("적용되었습니다.");
renderProgram(ret);
} else {
notifyWarning(ret.log || "적용에 실패했습니다.");
}
},
});
});
$("body").on("click", "#apply_new_season_btn", function (e) {
e.preventDefault();
const newSeason = document.getElementById("new_season").value;
if ($.isNumeric(newSeason) === false) {
notifyWarning("시즌은 숫자여야 합니다.");
return;
}
$.ajax({
url: `/${package_name}/ajax/apply_new_season`,
type: "POST",
cache: false,
data: { new_season: newSeason },
dataType: "json",
success: function (ret) {
if (ret.ret) {
notifySuccess("적용되었습니다.");
renderProgram(ret);
} else {
notifyWarning(ret.log || "적용에 실패했습니다.");
}
},
});
});
$("body").on("click", "#add_queue_btn", function (e) {
e.preventDefault();
const idx = Number($(this).data("idx"));
const episode = currentData?.episode?.[idx];
if (episode == null) {
notifyWarning("에피소드 정보를 찾지 못했습니다.");
return;
}
$.ajax({
url: `/${package_name}/ajax/add_queue`,
type: "POST",
cache: false,
data: { code: episode.code, data: JSON.stringify(episode) },
dataType: "json",
success: function (ret) {
if (ret.ret === "enqueue_db_append") {
notifySuccess("다운로드 작업에 추가했습니다.");
} else if (ret.ret === "enqueue_db_exist") {
notifyWarning("이미 DB에 있는 항목입니다.");
} else if (ret.ret === "db_completed") {
notifyWarning("이미 완료 기록이 있습니다.");
} else if (ret.ret === "queue_exist") {
notifyWarning("이미 대기열에 있습니다.");
} else if (ret.ret === "no_data") {
notifyWarning("에피소드 정보를 찾지 못했습니다.");
} else {
notifyWarning(ret.log || "대기열 추가에 실패했습니다.");
}
},
});
});
$("body").on("click", "#play_video_btn", function (e) {
e.preventDefault();
const idx = Number($(this).data("idx"));
const episode = currentData?.episode?.[idx];
if (episode == null) {
notifyWarning("에피소드 정보를 찾지 못했습니다.");
return;
}
requestPlay({
url: episode.url,
title: `${currentData.title} - ${episode.title}`,
});
});
$("body").on("click", "#check_download_btn", function (e) {
e.preventDefault();
const all = $('input[id^="checkbox_"]');
let str = "";
for (let i = 0; i < all.length; i += 1) {
if (all[i].checked) {
str += `${all[i].id.split("_")[1]},`;
}
}
if (str === "") {
notifyWarning("선택해 주세요.");
return;
}
$.ajax({
url: `/${package_name}/ajax/add_queue_checked_list`,
type: "POST",
cache: false,
data: { code: str },
dataType: "json",
success: function (ret) {
if (ret.ret === "success") {
notifySuccess(`${ret.log}개를 추가했습니다.`);
} else {
notifyWarning(ret.log || "대기열 추가에 실패했습니다.");
}
},
});
});
$("#go_modal_airing").click(function (e) {
e.preventDefault();
if (currentAiringData === null) {
getAiringList();
} else {
$("#airing_list").toggle();
}
});
$("#go_modal_airing").attr("class", "btn btn-primary");
$(function () {
if (typeof initialCodeFromQuery === "string" && initialCodeFromQuery.trim() !== "") {
setTimeout(() => {
runAnalysis();
}, 0);
}
});
})();