Compare commits

...

18 Commits

Author SHA1 Message Date
a8a64031bb Removing post
All checks were successful
Build and Deploy docker container / build (push) Successful in 5m16s
2025-04-15 22:17:11 -05:00
58b12534bb New blog post
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m19s
2025-02-07 00:15:58 -06:00
e1a3d59894 Adjusted umami host url
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m34s
2025-01-30 12:22:37 -06:00
6ba9b29daf changed the csp
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m6s
2025-01-28 18:21:52 -06:00
a4195f1ee5 updated linting issue
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m5s
2025-01-28 17:58:32 -06:00
ee886d4d13 updated umami
Some checks failed
Build and Deploy docker container / build (push) Has been cancelled
2025-01-28 17:57:51 -06:00
bbc1567ea4 updated headers to capitilize the letters
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m0s
2025-01-28 14:34:28 -06:00
418de925c4 added resume to navlinks
Some checks failed
Build and Deploy docker container / build (push) Has been cancelled
2025-01-28 14:32:15 -06:00
676b0c4ac1 hide nav link
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m9s
2025-01-28 04:47:12 -06:00
4bf423525a updated header and tagline
All checks were successful
Build and Deploy docker container / build (push) Successful in 5m4s
2025-01-28 04:41:15 -06:00
6dcfad7f0d qbitmaid
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m54s
2025-01-28 00:22:40 -06:00
78ac5b0423 publish qbitmaid 2025-01-28 00:22:19 -06:00
79b1b2db40 updated build.yml
All checks were successful
Build and Deploy docker container / build (push) Successful in 2m52s
2025-01-25 16:16:05 -06:00
30fdb4a3b9 test
All checks were successful
Build and Deploy docker container / build (push) Successful in 4m31s
2025-01-25 12:16:06 -06:00
292439fe68 test act runner 2025-01-25 12:09:18 -06:00
fafc6ed859 test build.yml 2025-01-25 12:04:25 -06:00
a79eac496a Merge pull request 'updated build.yml' (#2) from update-readme into main
Reviewed-on: #2
2025-01-25 12:01:04 -06:00
e06089e6a6 updated build.yml 2025-01-25 12:00:29 -06:00
10 changed files with 185 additions and 97 deletions

View File

@ -1,7 +1,10 @@
name: Build and Deploy docker container
'on':
on:
push:
branches: main
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
@ -56,7 +59,7 @@ jobs:
- name: Deploy app
uses: fjogeleit/http-request-action@v1
with:
url: 'http://192.168.4.11:7777/v1/update'
url: 'http://10.5.0.11:7777/v1/update'
method: GET
bearerToken: '${{ secrets.DEPLOYTOKEN }}'
timeout: 60000

View File

@ -1,17 +1 @@
{
"github": 1,
"guide": 4,
"next-js": 1,
"visual-studio-code": 1,
"tailwind-nextjs-starter-blog": 1,
"cygnus": 1,
"self-hosted": 1,
"server": 1,
"disney": 1,
"art": 1,
"store": 1,
"melis-sweetsimple": 1,
"python": 1,
"projects": 1,
"code": 1
}
{"tag1":1,"tag2":1,"tag3":1,"art":1,"store":1,"melis-sweetsimple":1,"visual-studio-code":1,"guide":4,"tailwind-nextjs-starter-blog":1,"disney":1,"github":1,"python":1,"projects":1,"code":1,"docker":1,"unraid":1,"cygnus":1,"self-hosted":1,"server":1,"next-js":1}

View File

@ -29,7 +29,7 @@ const Header = () => {
</div>
</Link>
<div className="flex items-center space-x-4 leading-5 sm:space-x-6">
<div className="no-scrollbar hidden max-w-40 items-center space-x-4 overflow-x-auto sm:flex sm:space-x-6 md:max-w-72 lg:max-w-96">
<div className="no-scrollbar hidden max-w-40 items-center space-x-4 overflow-x-scroll sm:flex sm:space-x-3 md:max-w-72 lg:max-w-96">
{headerNavLinks
.filter((link) => link.href !== '/')
.map((link) => (

10
data/blog/blogideas.mdx Normal file
View File

@ -0,0 +1,10 @@
---
title: 'idea'
date: '2025-1-1'
#lastmod: '202year-month-day'
draft: true
---
- split dns jonb.io vs int.jonb.io
- entertainment stack
<video controls autoPlay playsInline src="https://youtu.be/M1DJSXgJrDc?si=Rasg9u8muFno_si8"/>

View File

@ -1,16 +1,15 @@
---
title: 'Using Visual Studio Code to quickly write frontmatter'
date: '2024-11-12'
#lastmod: '202year-day-month'
tags: ['Visual Studio Code', 'guide', 'Tailwind Nextjs Starter Blog']
draft: true
date: '2024-2-7'
tags: ['Visual Studio Code', 'guide', 'code']
draft: false
summary: 'summary'
#images: ['/static/images/image.jpg',]
#authors: ['default',]
#layout: PostLayout
#canonicalUrl: https://jonb.io/blog/the-url-here
---
1. Paste the following into your md or mdx snippet file.In Visual Studio Code, go to File --> Preferences --> Configure Snippets.
2. To use a snippet, type the prefix and you will get snippet suggestions.
```json
{
"Blog Frontmatter": {

View File

@ -1,30 +0,0 @@
---
title: 'Introducing Melis Sweet&Simple'
date: '2024-11-14'
#lastmod: '202year-month-day'
tags: ['Art', 'store', 'Melis Sweet&Simple']
draft: false
summary: 'Elevate your Space with Bespoke Handcrafted Art from our Store that is Super Sweet and Simple.'
images: ['','','']
authors: ['default','meli']
layout: PostLayout # PostLayout, PostSimple and PostBanner
#canonicalUrl: https://jonb.io/blog/the-url-here
---
## Meli's Sweet&Simple
> Elevate your Space with Bespoke Handcrafted Art from our Store that is Super Sweet and Simple.
[Check it out!](https://melisweetsimple.jonb.io/)
<div className="-mx-2 flex flex-wrap overflow-hidden xl:-mx-2">
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
![walle](https://melisweetsimple.jonb.io/cdn/shop/files/FullSizeRender.heic?v=1730088140&width=2200)
</div>
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
![walleclose](https://melisweetsimple.jonb.io/cdn/shop/files/FullSizeRender_4297464f-1fe2-4809-9694-72058873bed8.heic?v=1730088140&width=2200)
</div>
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
![bird](https://melisweetsimple.jonb.io/cdn/shop/files/A_Whirlwind_6713f0cd-3ed1-4e75-a825-504ad1c88e66.png?v=1729026172&width=2048)
</div>
</div>

View File

@ -1,22 +1,24 @@
---
title: Qbitmaid
date: '2024-11-3'
lastmod: '2024-11-14'
tags: ['python', 'projects', 'code']
draft: true
lastmod: '2025-1-28'
tags: ['python', 'projects', 'code', 'docker', 'unraid']
draft: false
summary: How I used python to keep my torrents in check
layout: PostBanner
images: ['https://s3.jonb.io/cdn/projects/qbitmaid.jpg']
---
### qbit-maid
Development [^1] of qbitmaid was over the course of several months. At first, the project was called qbit-clean and didn't have all the features the project has now. The issue was mainly with my download cache in unraid being filled with torrents I no longer needed to seed[^2]. When I would get a notification from the server that the download cache was 95% full I would have to manually go to [qbittorrent](https://www.qbittorrent.org/), sort the torrents by age and remove the ones older than two weeks avoiding torrents I wanted to keep.
![qbittorrent](https://www.qbittorrent.org/img/qb_banner.webp)
[![qbittorrent](https://www.qbittorrent.org/img/qb_banner.webp)](https://www.qbittorrent.org/)
This was tedious. Very tedious. So I went off to do more work just to avoid a little.
This was tedious. Very tedious. So I went off to do more work just to avoid a little.`qbitmaid.py` is the main file that glues the project together. This was my first project where I heavily abstracted the design. More on this later. First, we'll connect to the API. I used an existing client [library](https://pypi.org/project/qbittorrent-api/2022.5.32/) that makes this process simpler than writing your own client.
`qbitmaid.py`[^3]
```python
...
class Qbt:
@ -31,38 +33,157 @@ class Qbt:
except qbittorrentapi.APIError as e:
self.tl.exception(e)
self.po.send_message(e, title="qbit-maid API ERROR")
# Pulling all torrent data
self.torrent_list = self.qbt_client.torrents_info()
#Main process block
if self.use_log:
list_qbit_api_info(self)
list_first_tor(self)
debug_torrent_list(self)
build_tor_list(self)
process_counts(self)
if self.use_log:
torrent_count(self)
tor_processor(self)
if self.use_log:
print_processor(self)
if self.delete_torrents:
tor_delete(self)
self.et = datetime.datetime.now()
get_script_runtime(self)
if self.use_pushover:
tor_notify_summary(self)
if self.use_apprise:
tor_notify_apprise(self, r, apprise_notify)
if self.use_healthcheck:
send_ping(self, r, self.healthcheck_url)
# Run
if __name__== "__main__":
Qbt()
...
```
This is the main file that glues the project together. This was my first project where I heavily
Then we use the api to make an list of the torrents:
```python
# Pulling all torrent data
self.torrent_list = self.qbt_client.torrents_info()
```
Next, we "sift" out torrents to be deleted. This was created with a positive sieve meaning we specify positive scenarios. In other words, I know which torrents I want to keep as opposed to the torrents I don't want. Theres pros and cons to both scenarios however in the long term a positive sieve is less work.
> `qlist.py` has functions at the bottom of the file that are referenced in the conditions. This very method of programming made it easy to write unit tests as I went.
```python
if is_preme(torrent['seeding_time'], self.min_age):
continue
```
```python
def is_preme(seeding_time, minage):
if seeding_time <= minage:
return True
```
When it comes across an item that meets certain criteria it will skip it. For instance, the example above checks to see if it's too soon to remove a torrent. This is because some [trackers][private tracker] require a minimum seed time. If you were to remove a torrent sooner than they require, it could lead to getting kicked.
`qlist.py` Has a couple jobs:
- Tag torrents according to how they should be treated.
- Sort
`qlist.py`
```python
...
def build_tor_list(self):
while self.torrent_list:
...
if is_tracker_blank(torrent['tracker']):
...
continue
elif is_cat_ignored(torrent['category'], self.cat_whitelist.values()):
...
continue
elif is_ignored_tag(self.ignored_tags.values(),torrent['tags']):
...
continue
if is_tag_blank(torrent['tags']):
...
if is_protected_tracker(torrent['tracker'], self.tracker_whitelist.values()):
self.qbt_client.torrents_add_tags(self.tracker_protected_tag,torrent['hash'])
elif is_not_protected_tracker(torrent['tracker'], self.tracker_whitelist.values()):
self.qbt_client.torrents_add_tags(self.tracker_non_protected_tag,torrent['hash'])
if is_preme(torrent['seeding_time'], self.min_age):
continue
elif is_protected_tracker(torrent['tracker'], self.tracker_whitelist.values()):
if is_tag_blank(torrent['tags']):
self.qbt_client.torrents_add_tags(self.tracker_protected_tag,torrent['hash'])
...
self.tracker_list.append(torrent)
elif is_not_protected_tracker(torrent['tracker'], self.tracker_whitelist.values()):
if is_tag_blank(torrent['tags']):
self.qbt_client.torrents_add_tags(self.tracker_non_protected_tag,torrent['hash'])
...
self.tracker_list.append(torrent)
```
In this case the items I want to keep stays. `qlist.py` passes the data over to `qprocess.py`. This is done through about 2 layers of abstraction. Unfortunatley, this type of programming makes it difficult to follow.
`qprocess.py` has four jobs:
- Collect telemetry
- Delete torrents if needed
- Enable debugging if needed
`qprocess.py`
```python
def tor_processor(self):
"""Main logic to sort through both self.tracker_nonprotected_list and self.tracker_protected_list
If torrent meets criteria for deletion, its infohash_v1 will be appended to self.torrent_hash_delete_list
"""
for canidate in self.tracker_list:
if self.enable_telemetry:
header = ['state','ratio','tags','added','hash','name','tracker']
row = [canidate['state'],canidate['ratio'],canidate["tags"],canidate['added_on'],canidate['infohash_v1'],canidate["name"][0:20],canidate['tracker']]
write_csv(self.cv,self.telemetry_outfile,header,row)
...
elif is_protected_over_ratio(canidate['ratio'], 1.05, self.tracker_protected_tag, canidate["tags"]):
if self.use_log:
self.tl.debug(f'["{canidate["name"][0:20]}..."] is above a 1.05 ratio({canidate["ratio"]}).')
self.torrent_hash_delete_list.append(canidate['infohash_v1'])
...
elif is_not_protected_tor(self.tracker_non_protected_tag, canidate["tags"]):
self.torrent_hash_delete_list.append(canidate['infohash_v1'])
...
else:
if self.enable_dragnet:
header = ['state','ratio','tags','added','thash','tname','trname']
row = [canidate['state'],canidate['ratio'],canidate["tags"],canidate['added_on'],canidate['infohash_v1'],canidate["name"][0:20],canidate['tracker']]
write_csv(self.cv,self.dragnet_outfile,header,row)
continue
```
I package this in a docker file:
```Dockerfile
FROM python:alpine3.18
WORKDIR /
COPY . opt
RUN apk add --no-cache supercronic
RUN pip install requests
RUN pip install qbittorrent-api
RUN chmod +x /opt/entrypoint.sh
CMD ["/opt/entrypoint.sh"]
```
Then use [Drone][drone] to package this into a container. This pushes the container to an [OCI repo][oci] in gitea. The application is configured through a toml file:
```toml
[qbittorrent]
host = "192.168.x.x"
port = 8080
username = "user"
password = "pass"
...
[healthcheck]
use_healthcheck = true
healthcheck_url = "https://example.com/ping/<uuid>>"
```
Using unraid has honestly been a delight. I had some performance issues but that was due to how I was using the storage pool.
![unraid](https://s3.jonb.io/cdn/blog/qbitmaid/unraid.png)
Finally, this same container will run the test cases in `test_qbitmaid.py`. This is handled by drone. So eachtime I push new code to a development branch on gitea, it creates a container to test and tests the code. Once I see that it has passed, I can merge the code to the main branch.
## Final Notes
I have been using this for over 2 years. It was a huge learning experience and my coding practices have evolved over my newer projects. While I did make this for my use mainly, feel free to try it out! If you have any questions, you can open an issue [here](https://git.jonb.io/jblu/qbit-maid/issues/new).
[^1]: The Source Code can be found [here][source-code].
[^2]: Private trackers require you to seed a torrent for a period of time. In my case, I have to seed for about 2 weeks or to a ratio of 1.
[^3]: Code has been removed for examples in this article.
[source-code]: https://git.jonb.io/jblu/qbit-maid
[source-code]: https://git.jonb.io/jblu/qbit-maid
[private tracker]: https://en.wikipedia.org/wiki/BitTorrent_tracker#Private_trackers
[drone]: https://www.drone.io/
[oci]: https://git.jonb.io/jblu/-/packages/container/qbit-maid/latest

View File

@ -1,10 +1,10 @@
const headerNavLinks = [
{ href: '/', title: 'Home' },
{ href: '/blog', title: 'Blog' },
// { href: 'https://git.jonb.io/', title: 'Code' },
{ href: '/likes', title: 'Likes' },
{ href: '/tags', title: 'Tags' },
{ href: '/projects', title: 'Projects' },
{ href: 'https://s3.jonb.io/cdn/author/Resume.pdf', title: 'Resume' },
{ href: '/about', title: 'About' },
]

View File

@ -3,7 +3,7 @@ const siteMetadata = {
title: 'Joyful Fixations',
author: 'Jonathan Branan',
headerTitle: 'Joyful Fixations',
description: "Thought's from someone with an internet connection.",
description: 'Things that bring me joy and intense focus.',
language: 'en-us',
theme: 'system', // system, dark or light
siteUrl: 'https://jonb.io',
@ -33,7 +33,8 @@ const siteMetadata = {
// We use an env variable for this site to avoid other users cloning our analytics ID
umamiWebsiteId: process.env.NEXT_UMAMI_ID, // e.g. 123e4567-e89b-12d3-a456-426614174000
// You may also need to overwrite the script if you're storing data in the US - ex:
// src: 'https://us.umami.is/script.js'
src: 'https://umami.jonb.io/script.js',
umamiHostUrl: 'https://umami.jonb.io',
// Remember to add 'us.umami.is' in `next.config.js` as a permitted domain for the CSP
},
// plausibleAnalytics: {

View File

@ -7,7 +7,7 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
// You might need to insert additional domains in script-src if you are using external services
const ContentSecurityPolicy = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline' giscus.app analytics.umami.is;
script-src 'self' 'unsafe-eval' 'unsafe-inline' giscus.app umami.jonb.io;
style-src 'self' 'unsafe-inline';
img-src * blob: data:;
media-src s3.jonb.io* melisweetsimple.jonb.io*;