Skip to content

Commit 6404b8b

Browse files
committed
feat: docker
1 parent a932586 commit 6404b8b

File tree

7 files changed

+221
-4
lines changed

7 files changed

+221
-4
lines changed

.dockerignore

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Include any files or directories that you don't want to be copied to your
2+
# container here (e.g., local build artifacts, temporary files, etc.).
3+
#
4+
# For more help, visit the .dockerignore file reference guide at
5+
# https://docs.docker.com/go/build-context-dockerignore/
6+
7+
**/.DS_Store
8+
**/.classpath
9+
**/.dockerignore
10+
**/.env
11+
**/.git
12+
**/.gitignore
13+
**/.project
14+
**/.settings
15+
**/.toolstarget
16+
**/.vs
17+
**/.vscode
18+
**/*.*proj.user
19+
**/*.dbmdl
20+
**/*.jfm
21+
**/charts
22+
**/docker-compose*
23+
**/compose.y*ml
24+
**/Dockerfile*
25+
**/node_modules
26+
**/npm-debug.log
27+
**/secrets.dev.yaml
28+
**/values.dev.yaml
29+
/bin
30+
/target
31+
LICENSE
32+
README.md

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "server"
3-
version = "1.2.0"
3+
version = "1.3.0"
44
edition = "2021"
55

66
[dependencies]

Dockerfile

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
FROM rust:1.90.0-alpine3.22 AS builder
2+
3+
# Install build dependencies for Rust + musl as we're using alpine
4+
RUN apk add --no-cache \
5+
musl-dev \
6+
gcc \
7+
g++ \
8+
libc-dev \
9+
make \
10+
pkgconfig \
11+
openssl-dev \
12+
openssl-libs-static
13+
14+
WORKDIR /usr/src/app
15+
16+
# Copy manifest files first for dependency caching
17+
COPY Cargo.toml Cargo.lock ./
18+
19+
# Build the app to cache dependencies
20+
COPY . .
21+
RUN cargo build --release --locked
22+
RUN rm -rf src
23+
24+
FROM alpine:3.22
25+
26+
# Install runtime dependencies
27+
RUN apk add --no-cache ca-certificates
28+
29+
# Copy the compiled binary from builder stage
30+
COPY --from=builder /usr/src/app/target/release/server /usr/local/bin/app
31+
32+
# Expose default port
33+
EXPOSE 8080
34+
35+
# Start the app
36+
CMD ["app"]

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Get the latest five uploads for any YouTube channel!
44

55
# How to use
66

7+
## Using a browser
8+
9+
You can go to [the site](https://latestvid.stats100.xyz)
10+
11+
## In a program using a Fetch API
12+
713
It's very simple, just make a request to `https://latestvid.stats100.xyz/get/<CHANNEL_ID_HERE>`
814
For example, to get the latest uploads for [MrBeast](https://youtube.com/@mrbeast), just have to make a simple request to `https://latestvid.stats100.xyz/get/UCX6OQ3DkcsbYNE6H8uQQuVA` and get an example response like this:
915

@@ -118,3 +124,9 @@ As a reminder, queries can be stacked: `?type=shorts&maxresults=2`
118124
}
119125
]
120126
```
127+
128+
# Docker
129+
130+
Docker is not required. I just use it because Nix is very unhappy on my server.
131+
132+
If you do use to use Docker, the application runs on port 8080 by default

src/index.html

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>Latest YouTube Videos API</title>
8+
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
9+
</head>
10+
11+
<body
12+
class="bg-gray-100 text-gray-900 dark:bg-gray-900 dark:text-gray-100 flex items-center justify-center min-h-screen">
13+
<div class="w-full max-w-3xl px-4">
14+
<h1 class="text-5xl font-extrabold text-blue-600 dark:text-blue-400 mb-4 text-center">
15+
Latest YouTube Videos API
16+
</h1>
17+
<p class="text-lg text-gray-700 dark:text-gray-300 mb-6 text-center">
18+
Fetch and display the latest videos from your favourite channels.
19+
</p>
20+
<form id="fetch-videos-form" class="space-y-4">
21+
<div>
22+
<label for="channel-id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
23+
YouTube Channel ID
24+
</label>
25+
<input type="text" id="channel-id" name="channel-id" placeholder="Enter Channel ID"
26+
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-700 dark:placeholder-gray-500 dark:text-gray-100">
27+
</div>
28+
<div>
29+
<label for="show-options" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
30+
Show Optional Fields
31+
</label>
32+
<input type="checkbox" id="show-options" name="show-options"
33+
class="mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 dark:bg-gray-800 dark:border-gray-700">
34+
</div>
35+
<div id="optional-fields" class="space-y-4 hidden">
36+
<div>
37+
<label for="video-count" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
38+
Video Count (1-50)
39+
</label>
40+
<input type="number" id="video-count" name="video-count" min="1" max="50"
41+
placeholder="Enter video count"
42+
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-700 dark:placeholder-gray-500 dark:text-gray-100">
43+
</div>
44+
<div>
45+
<label for="type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Type</label>
46+
<select id="type" name="type"
47+
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-100">
48+
<option value="all">All</option>
49+
<option value="video">Video</option>
50+
<option value="shorts">Shorts</option>
51+
<option value="streams">Streams</option>
52+
</select>
53+
</div>
54+
</div>
55+
<button type="submit"
56+
class="w-full px-4 py-2 bg-blue-600 text-white font-medium rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-blue-500 dark:hover:bg-blue-600">
57+
Fetch Videos
58+
</button>
59+
</form>
60+
61+
<div id="video-list" class="mt-6 grid gap-4 sm:grid-cols-1 md:grid-cols-2"></div>
62+
</div>
63+
64+
<script>
65+
const showOptionsCheckbox = document.getElementById('show-options');
66+
const optionalFields = document.getElementById('optional-fields');
67+
const videoList = document.getElementById('video-list');
68+
69+
showOptionsCheckbox.addEventListener('change', () => {
70+
optionalFields.classList.toggle('hidden', !showOptionsCheckbox.checked);
71+
});
72+
73+
const form = document.getElementById('fetch-videos-form');
74+
form.addEventListener('submit', async (event) => {
75+
event.preventDefault();
76+
77+
const channelId = document.getElementById('channel-id').value;
78+
const videoCount = document.getElementById('video-count').value || 5;
79+
const type = document.getElementById('type').value || 'all';
80+
81+
if (!channelId) {
82+
alert('Please enter a YouTube Channel ID.');
83+
return;
84+
}
85+
86+
try {
87+
const response = await fetch(`/get/${channelId}?type=${type}&maxresults=${videoCount}`);
88+
if (!response.ok) throw new Error('Failed to fetch videos');
89+
90+
const data = await response.json();
91+
renderVideos(data);
92+
} catch (error) {
93+
console.error('Error fetching videos:', error);
94+
alert('An error occurred while fetching videos.');
95+
}
96+
});
97+
98+
function renderVideos(videos) {
99+
videoList.innerHTML = '';
100+
videos.forEach(video => {
101+
const videoCard = document.createElement('a');
102+
videoCard.href = `https://www.youtube.com/watch?v=${video.videoId}`;
103+
videoCard.target = '_blank';
104+
videoCard.className =
105+
"block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow overflow-hidden";
106+
107+
videoCard.innerHTML = `
108+
<img src="https://i.ytimg.com/vi/${video.videoId}/hqdefault.jpg" alt="${video.title}" class="w-full h-40 object-cover">
109+
<div class="p-4">
110+
<h2 class="text-lg font-semibold text-blue-600 dark:text-blue-400 hover:underline line-clamp-2">${video.title}</h2>
111+
<p class="text-xs text-gray-500 mt-1">${new Date(video.publishedAt).toLocaleString()}</p>
112+
</div>
113+
`;
114+
videoList.appendChild(videoCard);
115+
});
116+
}
117+
</script>
118+
</body>
119+
120+
</html>

src/main.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,11 @@ async fn get_videos(
3333
.map(|v| v.clamp(1, 50))
3434
.unwrap_or(5);
3535

36-
3736
let url = format!(
3837
"https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId={}&key={}&maxResults={}",
3938
id, *API_KEY.lock().unwrap(), max_results
4039
);
41-
40+
4241
println!(
4342
"Fetching videos for playlist {} | Max results: {} | URL: {}",
4443
id, max_results, url
@@ -100,6 +99,12 @@ async fn get_videos(
10099
response
101100
}
102101

102+
async fn serve_index() -> impl Responder {
103+
HttpResponse::Ok()
104+
.content_type("text/html")
105+
.body(include_str!("index.html"))
106+
}
107+
103108
#[actix_web::main]
104109
async fn main() -> std::io::Result<()> {
105110
dotenv::dotenv().ok();
@@ -121,6 +126,18 @@ async fn main() -> std::io::Result<()> {
121126
.allow_any_method()
122127
.allow_any_header(),
123128
)
129+
.route("/", web::get().to(serve_index))
130+
.route("/index.html", web::get().to(serve_index))
131+
.route("/docs", web::get().to(|| async {
132+
HttpResponse::Found()
133+
.insert_header(("Location", "https://github.com/GalvinPython/latest-uploads-api#latest-youtube-uploads"))
134+
.finish()
135+
}))
136+
.route("/docs/", web::get().to(|| async {
137+
HttpResponse::Found()
138+
.insert_header(("Location", "https://github.com/GalvinPython/latest-uploads-api#latest-youtube-uploads"))
139+
.finish()
140+
}))
124141
.route("/get/{id}", web::get().to(get_videos))
125142
.route("/get/{id}/", web::get().to(get_videos))
126143
})

0 commit comments

Comments
 (0)