Skip to content

Commit

Permalink
Implement sync image with youtube
Browse files Browse the repository at this point in the history
  • Loading branch information
VovaStelmashchuk committed Dec 8, 2024
1 parent cba9622 commit 2126796
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 109 deletions.
64 changes: 21 additions & 43 deletions src/minio/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ import {
HeadObjectCommand,
} from "@aws-sdk/client-s3";

import { Upload } from "@aws-sdk/lib-storage";
import dotenv from "dotenv";
import Fs from "fs";
import url from "url";
import ytdl from "ytdl-core";
import { Upload } from "@aws-sdk/lib-storage";

dotenv.config();

Expand All @@ -27,51 +25,31 @@ const client = new S3Client({
s3ForcePathStyle: true,
});

export async function streamYoutubeVideoAudioToS3(videoId, key) {
try {
const youtubeUrl = `https://www.youtube.com/watch?v=${videoId}`;
const output = Fs.createWriteStream("/tmp/audio.mp4");

ytdl(youtubeUrl)
.on("error", (err) => {
console.error("Error during download:", err);
})
.on("info", (info) => {
console.log(`Downloading audio from: ${info.videoDetails.title}`);
})
.pipe(output)
.on("finish", () => {
console.log(`Audio downloaded successfully to: ${outputPath}`);
});
} catch (error) {
console.error("Error:", error);
export async function downloadAndUploadImage(imageUrl, key) {
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error(
`Failed to download image: ${response.status} ${response.statusText}`
);
}
}

async function uploadFileStream(key, body, contentType) {
try {
const upload = new Upload({
client,
params: {
Bucket: process.env.S3_BUCKET_NAME,
Key: key,
Body: body,
ContentType: contentType,
},
});

upload.on("httpUploadProgress", (progress) => {
console.log(
`Upload progress: ${progress.loaded}/${
progress.total || "unknown total"
} bytes`
);
});
const upload = new Upload({
client,
params: {
Bucket: process.env.S3_BUCKET_NAME,
Key: key,
Body: response.body,
ContentType: "image/jpeg",
},
});

try {
await upload.done();
console.log(`File uploaded successfully, key = ${key}`);
console.log(
`Image streamed from ${imageUrl} and uploaded to S3 with key: ${key}`
);
} catch (error) {
console.error("Error uploading file:", error);
console.error(`Error uploading image stream: ${error.message}`);
throw error;
}
}
Expand Down
91 changes: 71 additions & 20 deletions src/routers/admin/sync.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { google } from "googleapis";
import { getShowBySlug } from "../../core/showRepo.js";
import { Database } from "../../core/client.js";
import { streamYoutubeVideoAudioToS3 } from "../../minio/utils.js";
import { downloadAndUploadImage } from "../../minio/utils.js";
import slugify from "slugify";
import dotenv from "dotenv";

dotenv.config();

const youtubeApiKey = process.env.YOUTUBE_API_KEY;
const startS3Url = process.env.S3_START_URL;

async function getPlaylistItems(playlistId, nextPageToken) {
const youtube = google.youtube({
Expand All @@ -25,6 +26,34 @@ async function getPlaylistItems(playlistId, nextPageToken) {
return response.data;
}

function buildDescription(description, videoId) {
const urlRegex = /(https?:\/\/[^\s]+)/g;

let processedText = description
.replace(urlRegex, function (url) {
return `<a class="text-green-500" href="${url}" target="_blank">${url}</a>`;
})
.replace(/\n/g, "<br>");

processedText =
`<a class="text-green-500" href="https://www.youtube.com/watch?v=${videoId}" > Подивитись відео на YouTube </a><br>` +
processedText;

return processedText;
}

function buildShortDescription(description) {
let shortDescription = description;
const firstTwoZeroIndex = description.indexOf("00:00");
if (firstTwoZeroIndex !== -1) {
shortDescription = description.slice(firstTwoZeroIndex);
}
// remove all time stamps in format hh:mm:ss and '-' character
shortDescription = shortDescription.replace(/(\d{2}:\d{2}:\d{2})/g, "");
shortDescription = shortDescription.replace(/-/g, "");
return shortDescription;
}

async function performShowSyncHandler(request, h) {
const showSlug = request.params.showSlug;
const show = await getShowBySlug(showSlug);
Expand All @@ -33,8 +62,6 @@ async function performShowSyncHandler(request, h) {
let nextPageToken = null;
let items = [];

console.log("Syncing show: ", showSlug, playlistId);

do {
const response = await getPlaylistItems(playlistId, nextPageToken);
items = items.concat(response.items);
Expand All @@ -46,22 +73,28 @@ async function performShowSyncHandler(request, h) {
if (!thumbnail) {
thumbnail = item.snippet.thumbnails.standard;
}
const youtubeDescription = item.snippet.description;
const videoId = item.snippet.resourceId.videoId;
return {
youtube: {
videoId: videoId,
title: item.snippet.title,
description: youtubeDescription,
publishedAt: item.snippet.publishedAt,
thumbnail: thumbnail.url,
},
slug: slugify(item.snippet.title, { lower: true, strict: true }),
videoId: item.snippet.resourceId.videoId,
title: item.snippet.title,
description: item.snippet.description,
publishedAt: item.snippet.publishedAt,
thumbnail: thumbnail.url,
shortDescription: buildShortDescription(youtubeDescription),
description: buildDescription(youtubeDescription, videoId),
};
});

Database.collection("shows").updateOne(
await Database.collection("shows").updateOne(
{ slug: showSlug },
{
$set: {
youtubeVideoItems: youtubeVideoItems,
lastSyncTime: new Date(),
items: youtubeVideoItems,
},
}
);
Expand All @@ -73,11 +106,19 @@ async function syncPageHandler(request, h) {
const showSlug = request.params.showSlug;
const show = await getShowBySlug(showSlug);

const items = show.items.map((item) => ({
...item,
title: item.youtube.title,
description: item.youtube.description,
imageUrl: item.youtube.thumbnail,
refreshMediaUrl: `/admin/show/${showSlug}/${item.slug}/refresh-media`,
}));

return h.view(
"admin/episode_list",
{
pageTitle: show.showName,
posts: show.youtubeVideoItems,
posts: items,
performSyncUrl: `/admin/show/${showSlug}/perform-sync`,
lastSyncTime: show.lastSyncTime,
},
Expand All @@ -89,21 +130,31 @@ async function syncPageHandler(request, h) {

async function syncEpisodeHandler(request, h) {
const showSlug = request.params.showSlug;
const videoId = request.params.videoId;
const episodeSlug = request.params.episodeSlug;
const show = await getShowBySlug(showSlug);

const episode = show.youtubeVideoItems.find(
(item) => item.videoId === videoId
);
const episode = show.items.find((item) => item.slug === episodeSlug);

console.log("Syncing episode: ", episode);

await streamYoutubeVideoAudioToS3(
// Sync episode audio - video
/*await streamYoutubeVideoAudioToS3(
videoId,
`v2/${showSlug}/episodes/${videoId}.mp3`
);*/

const key = `v2/${showSlug}/episodes/${episodeSlug}.jpg`;
downloadAndUploadImage(episode.youtube.thumbnail, key);

await Database.collection("shows").updateOne(
{ slug: showSlug, "items.slug": episodeSlug },
{
$set: {
"items.$.image": key,
},
}
);

return { message: "Syncing episode" };
return h.response().code(200);
}

export function syncApis(server) {
Expand All @@ -118,10 +169,10 @@ export function syncApis(server) {

server.route({
method: "POST",
path: "/admin/show/{showSlug}/{videoId}/perform-sync",
path: "/admin/show/{showSlug}/{episodeSlug}/refresh-media",
handler: syncEpisodeHandler,
options: {
auth: false, //"adminSession",
auth: "adminSession",
},
});

Expand Down
28 changes: 3 additions & 25 deletions src/routers/details.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,22 @@
import { getShowInfo } from "../core/podcastRepo.js";

function buildDescription(description, videoId) {
const urlRegex = /(https?:\/\/[^\s]+)/g;

let processedText = description
.replace(urlRegex, function (url) {
return `<a class="text-green-500" href="${url}" target="_blank">${url}</a>`;
})
.replace(/\n/g, "<br>");

processedText =
`<a class="text-green-500" href="https://www.youtube.com/watch?v=${videoId}" > Подивитись відео на YouTube </a><br>` +
processedText;

return processedText;
}

async function podcastDetailsHandler(request, h) {
const host = request.headers.host;
const showInfo = await getShowInfo(host);
const slug = request.params.slug;

const episode = showInfo.youtubeVideoItems.find(
(episode) => episode.slug === slug
);
const episode = showInfo.items.find((episode) => episode.slug === slug);

console.log("episode", episode);

return h.view(
"podcastDetails",
{
videoId: episode.videoId,
videoId: episode.youtube.videoId,
showName: showInfo.showName,
header_links: showInfo.links,
description: buildDescription(
episode.description || "",
episode.videoId
).trim(),
description: episode.description,
title: episode.title,
links: [],
},
{
layout: "layout",
Expand Down
32 changes: 14 additions & 18 deletions src/routers/home.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,24 @@ async function homeHandler(request, h) {
);
}

function buildShortDescription(description) {
let shortDescription = description;
const firstTwoZeroIndex = description.indexOf("00:00");
if (firstTwoZeroIndex !== -1) {
shortDescription = description.slice(firstTwoZeroIndex);
}
// remove all time stamps in format hh:mm:ss and '-' character
shortDescription = shortDescription.replace(/(\d{2}:\d{2}:\d{2})/g, "");
shortDescription = shortDescription.replace(/-/g, "");
return shortDescription;
}

async function podcastListHandler(request, h) {
const host = request.headers.host;
const showInfo = await getShowInfo(host);

const posts = showInfo.youtubeVideoItems.map((video) => ({
url: `/podcast/${video.slug}`,
imageUrl: `${startUrl}${showInfo.showLogoUrl}`,
title: video.title,
chartersDescription: buildShortDescription(video.description),
}));
const posts = showInfo.items.map((item) => {
let imageUrl;
if (item.image) {
imageUrl = `${startUrl}/${item.image}`;
} else {
imageUrl = `${startUrl}${showInfo.showLogoUrl}`;
}
return {
url: `/podcast/${item.slug}`,
imageUrl: `${imageUrl}`,
title: item.youtube.title,
chartersDescription: item.shortDescription,
};
});

return h.view(
"podcastList",
Expand Down
27 changes: 24 additions & 3 deletions src/templates/pages/admin/episode_list.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
<div class="p-2">
<div class="grid grid-cols-1 gap-4">
{{#each posts}}
<div class="no-underline bg-white p-8 rounded shadow-md">
<h2 class="text-xl font-bold line-clamp-3">{{title}}</h2>
<p class="text-gray-600 mt-2">{{description}}</p>
<div
class="no-underline bg-white p-6 rounded shadow-md flex gap-4 items-start"
>
<div class="flex flex-col items-start">
<img
src="{{imageUrl}}"
alt="{{title}}"
class="w-24 object-cover rounded"
/>
<button
hx-swap="none"
hx-post="{{refreshMediaUrl}}"
class="common-button w-full px-4 py-2 mt-4"
>
Refresh Media
</button>
</div>

<div class="flex-1">
<h2 class="text-xl font-bold">{{title}}</h2>
<p class="text-gray-600 mt-2">{{description}}</p>
</div>
</div>
{{/each}}
</div>

<button
hx-swap="none"
hx-post="{{performSyncUrl}}"
class="common-button w-full px-4 py-2 my-2"
>
Run sync
</button>

<div class="text-gray-600 text-sm mt-2">
Last playlist sync: {{lastSyncTime}}
</div>
Expand Down

0 comments on commit 2126796

Please sign in to comment.