Compare commits
22 Commits
30e26313b0
...
main
Author | SHA1 | Date | |
---|---|---|---|
a8a64031bb | |||
58b12534bb | |||
e1a3d59894 | |||
6ba9b29daf | |||
a4195f1ee5 | |||
ee886d4d13 | |||
bbc1567ea4 | |||
418de925c4 | |||
676b0c4ac1 | |||
4bf423525a | |||
6dcfad7f0d | |||
78ac5b0423 | |||
79b1b2db40 | |||
30fdb4a3b9 | |||
292439fe68 | |||
fafc6ed859 | |||
a79eac496a | |||
e06089e6a6 | |||
1debc3e50c | |||
34d1ec5006 | |||
a1a3e21266 | |||
6bfe06f65c |
@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
.eslintrc.js
|
42
.eslintrc.js
@ -1,42 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
env: {
|
||||
browser: true,
|
||||
amd: true,
|
||||
node: true,
|
||||
es6: true,
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'next',
|
||||
'next/core-web-vitals',
|
||||
],
|
||||
parserOptions: {
|
||||
project: true,
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'jsx-a11y/anchor-is-valid': [
|
||||
'error',
|
||||
{
|
||||
components: ['Link'],
|
||||
specialLink: ['hrefLeft', 'hrefRight'],
|
||||
aspects: ['invalidHref', 'preferButton'],
|
||||
},
|
||||
],
|
||||
'react/prop-types': 0,
|
||||
'@typescript-eslint/no-unused-vars': 0,
|
||||
'react/no-unescaped-entities': 0,
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
},
|
||||
}
|
@ -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
|
@ -1,6 +1,8 @@
|
||||

|
||||
|
||||
# Tailwind Nextjs Starter Blog
|
||||
# Joyful Fixations
|
||||
|
||||
> This template has been adapted for my needs. Please view the upstream if you wish to create a similar site.
|
||||
|
||||
[](https://GitHub.com/timlrx/tailwind-nextjs-starter-blog/stargazers/)
|
||||
[](https://GitHub.com/timlrx/tailwind-nextjs-starter-blog/network/)
|
||||
|
@ -67,29 +67,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
className={`${space_grotesk.variable} scroll-smooth`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="76x76"
|
||||
href={`${basePath}/static/favicons/apple-touch-icon.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href={`${basePath}/static/favicons/favicon-32x32.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href={`${basePath}/static/favicons/favicon-16x16.png`}
|
||||
/>
|
||||
<link rel="manifest" href={`${basePath}/static/favicons/site.webmanifest`} />
|
||||
<link
|
||||
rel="mask-icon"
|
||||
href={`${basePath}/static/favicons/safari-pinned-tab.svg`}
|
||||
color="#5bbad5"
|
||||
/>
|
||||
<link rel="icon" type="image/png" href="/static/favicons/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicons/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/static/favicons/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicons/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/static/favicons/site.webmanifest" />
|
||||
<meta name="msapplication-TileColor" content="#000000" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fff" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000" />
|
||||
|
@ -1 +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,"cygnus":1,"self-hosted":1,"server":1,"next-js":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}
|
@ -17,6 +17,7 @@ export default function Footer() {
|
||||
<SocialIcon kind="instagram" href={siteMetadata.instagram} size={6} />
|
||||
<SocialIcon kind="threads" href={siteMetadata.threads} size={6} />
|
||||
<SocialIcon kind="medium" href={siteMetadata.medium} size={6} />
|
||||
<SocialIcon kind="resume" href={siteMetadata.resume} size={6} />
|
||||
</div>
|
||||
<div className="mb-2 flex space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div>{siteMetadata.author}</div>
|
||||
@ -26,7 +27,7 @@ export default function Footer() {
|
||||
<Link href="/">{siteMetadata.title}</Link>
|
||||
</div>
|
||||
<div className="mb-8 text-sm text-gray-500 dark:text-gray-400">
|
||||
<Link href="https://github.com/timlrx/tailwind-nextjs-starter-blog">Theme</Link>
|
||||
<Link href="https://git.jonb.io/jblu/jonbio">Theme</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -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) => (
|
||||
|
@ -102,3 +102,12 @@ export function Medium(svgProps: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Resume(svgProps: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...svgProps}>
|
||||
<title>Resume</title>
|
||||
<path d="M14.727 6.727H14V0H4.91c-.905 0-1.637.732-1.637 1.636v20.728c0 .904.732 1.636 1.636 1.636h14.182c.904 0 1.636-.732 1.636-1.636V6.727h-6zm-.545 10.455H7.09v-1.364h7.09v1.364zm2.727-3.273H7.091v-1.364h9.818v1.364zm0-3.273H7.091V9.273h9.818v1.363zM14.727 6h6l-6-6v6z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
Threads,
|
||||
Instagram,
|
||||
Medium,
|
||||
Resume,
|
||||
} from './icons'
|
||||
|
||||
const components = {
|
||||
@ -24,6 +25,7 @@ const components = {
|
||||
threads: Threads,
|
||||
instagram: Instagram,
|
||||
medium: Medium,
|
||||
resume: Resume,
|
||||
}
|
||||
|
||||
type SocialIconProps = {
|
||||
|
@ -140,6 +140,7 @@ export const Authors = defineDocumentType(() => ({
|
||||
linkedin: { type: 'string' },
|
||||
github: { type: 'string' },
|
||||
layout: { type: 'string' },
|
||||
resume: { type: 'string' },
|
||||
},
|
||||
computedFields,
|
||||
}))
|
||||
|
@ -2,15 +2,16 @@
|
||||
name: Jonathan Branan
|
||||
avatar: https://s3.jonb.io/cdn/author/2.JPG
|
||||
occupation: Software Engineer
|
||||
company: Fortra
|
||||
email: jonathan.branan@fortra.com
|
||||
# company:
|
||||
email: jonbranan@gmail.com
|
||||
linkedin: https://www.linkedin.com/in/jonathanbranan/
|
||||
github: https://github.com/jonbranan
|
||||
resume: https://s3.jonb.io/cdn/author/Resume.pdf
|
||||
---
|
||||
|
||||
Jonathan Branan is a Software Engineer at Fortra. He is mostly self-taught however he did attend a vocational class at Rackspace Open Cloud Academy.
|
||||
Jonathan Branan is a Software Engineer. He is mostly self-taught however he did attend a vocational class at Rackspace Open Cloud Academy.
|
||||
|
||||
He has worked for Geek Squad repairing computers, GlobalScape as a Lead of the Client Services department and as a Product Owner of MFT applications at Fortra.
|
||||
He has worked for Geek Squad repairing computers, GlobalScape as a Lead of the Client Services department, a Product Owner of MFT applications and as a Software Engineer at Fortra.
|
||||
|
||||
He currently lives in San Antonio, Texas with his wife and two dogs. He enjoys Basketball, Video games, cooking, camping(backpacking, glamping), watching movies,
|
||||
making mixed drinks and building Lego's. Jonathan and his wife like to frequently travel.
|
||||
|
10
data/blog/blogideas.mdx
Normal 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"/>
|
@ -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": {
|
||||
|
@ -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">
|
||||

|
||||
</div>
|
||||
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
|
||||

|
||||
</div>
|
||||
<div className="my-1 w-full overflow-hidden px-2 xl:my-1 xl:w-1/2 xl:px-2">
|
||||

|
||||
</div>
|
||||
</div>
|
||||
|
@ -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.
|
||||
|
||||

|
||||
[](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.
|
||||
|
||||

|
||||
|
||||
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
|
||||
[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
|
||||
|
@ -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' },
|
||||
]
|
||||
|
||||
|
3
data/logo copy.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="344.5639097744361 330.27819548872174 111.73684210526318 91.21804511278197" width="53.87" height="43.61"><defs><path d="M453.3 331.28L453.3 359.85L388.64 418.5L388.64 388.42L453.3 331.28Z" id="aFZf6T5ED"></path><linearGradient id="gradientb2ThqnP5Op" gradientUnits="userSpaceOnUse" x1="420.97" y1="331.28" x2="420.97" y2="418.5"><stop style="stop-color: #06b6d4;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #67e8f9;stop-opacity: 1" offset="100%"></stop></linearGradient><path d="M410.23 331.28L410.23 359.85L345.56 418.5L345.56 388.42L410.23 331.28Z" id="a9fehgwfM"></path><linearGradient id="gradientk1wNV9Ostb" gradientUnits="userSpaceOnUse" x1="377.89" y1="331.28" x2="377.89" y2="418.5"><stop style="stop-color: #06b6d4;stop-opacity: 1" offset="0%"></stop><stop style="stop-color: #67e8f9;stop-opacity: 1" offset="100%"></stop></linearGradient></defs><g><g><use xlink:href="#aFZf6T5ED" opacity="1" fill="url(#gradientb2ThqnP5Op)"></use></g><g><use xlink:href="#a9fehgwfM" opacity="1" fill="url(#gradientk1wNV9Ostb)"></use></g></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 38 KiB |
@ -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',
|
||||
@ -21,6 +21,7 @@ const siteMetadata = {
|
||||
// threads: 'https://www.threads.net',
|
||||
// instagram: 'https://www.instagram.com',
|
||||
// medium: 'https://medium.com',
|
||||
resume: 'https://s3.jonb.io/cdn/author/Resume.pdf',
|
||||
locale: 'en-US',
|
||||
// set to true if you want a navbar fixed to the top
|
||||
stickyNav: true,
|
||||
@ -32,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: {
|
||||
|
70
eslint.config.mjs
Normal file
@ -0,0 +1,70 @@
|
||||
import typescriptEslint from '@typescript-eslint/eslint-plugin'
|
||||
import globals from 'globals'
|
||||
import tsParser from '@typescript-eslint/parser'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import js from '@eslint/js'
|
||||
import { FlatCompat } from '@eslint/eslintrc'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
})
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...compat.extends(
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'next',
|
||||
'next/core-web-vitals'
|
||||
),
|
||||
{
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.amd,
|
||||
...globals.node,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: 5,
|
||||
sourceType: 'commonjs',
|
||||
|
||||
parserOptions: {
|
||||
project: true,
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
|
||||
'jsx-a11y/anchor-is-valid': [
|
||||
'error',
|
||||
{
|
||||
components: ['Link'],
|
||||
specialLink: ['hrefLeft', 'hrefRight'],
|
||||
aspects: ['invalidHref', 'preferButton'],
|
||||
},
|
||||
],
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
},
|
||||
},
|
||||
]
|
@ -9,7 +9,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function AuthorLayout({ children, content }: Props) {
|
||||
const { name, avatar, occupation, company, email, twitter, linkedin, github } = content
|
||||
const { name, avatar, occupation, company, email, twitter, linkedin, github, resume } = content
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -38,6 +38,7 @@ export default function AuthorLayout({ children, content }: Props) {
|
||||
<SocialIcon kind="github" href={github} />
|
||||
<SocialIcon kind="linkedin" href={linkedin} />
|
||||
<SocialIcon kind="x" href={twitter} />
|
||||
<SocialIcon kind="resume" href={resume} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="prose max-w-none pb-8 pt-8 dark:prose-invert xl:col-span-2">
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
2
next-env.d.ts
vendored
@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
@ -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*;
|
||||
|
16561
package-lock.json
generated
Normal file
10
package.json
@ -13,7 +13,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "2.2.0",
|
||||
"@next/bundle-analyzer": "15.0.2",
|
||||
"@next/bundle-analyzer": "15.1.4",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"autoprefixer": "^10.4.13",
|
||||
@ -24,12 +24,13 @@
|
||||
"gray-matter": "^4.0.2",
|
||||
"hast-util-from-html-isomorphic": "^2.0.0",
|
||||
"image-size": "1.0.0",
|
||||
"next": "15.0.2",
|
||||
"next": "15.1.4",
|
||||
"next-contentlayer2": "0.5.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"pliny": "0.4.0",
|
||||
"postcss": "^8.4.24",
|
||||
"react": "rc",
|
||||
"react-doc-viewer": "^0.1.14",
|
||||
"react-dom": "rc",
|
||||
"reading-time": "1.5.0",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
@ -47,6 +48,8 @@
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@svgr/webpack": "^8.0.1",
|
||||
"@types/mdx": "^2.0.12",
|
||||
"@types/react": "^18.2.73",
|
||||
@ -54,9 +57,10 @@
|
||||
"@typescript-eslint/parser": "^8.12.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-next": "15.0.2",
|
||||
"eslint-config-next": "15.1.4",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.0",
|
||||
"globals": "^15.12.0",
|
||||
"husky": "^9.0.0",
|
||||
"lint-staged": "^13.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.9 KiB |
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#000000</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.4 KiB |
BIN
public/static/favicons/favicon-96x96.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 4.3 KiB |
@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="107.000000pt" height="107.000000pt" viewBox="0 0 107.000000 107.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,107.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M609 942 c-17 -18 -95 -88 -289 -258 -117 -102 -177 -155 -204 -179
|
||||
-6 -5 -31 -28 -56 -50 l-45 -39 -1 -144 -1 -143 21 18 c11 10 25 23 31 28 7 6
|
||||
46 41 88 79 86 77 86 77 216 196 52 47 103 93 115 103 11 10 50 45 86 77 l65
|
||||
59 -1 131 c0 71 -3 132 -5 135 -3 2 -12 -3 -20 -13z"/>
|
||||
<path d="M929 857 c-64 -56 -145 -128 -180 -159 -35 -31 -75 -66 -89 -78 -55
|
||||
-47 -68 -59 -150 -131 l-85 -76 2 -129 c1 -71 2 -135 2 -143 1 -11 5 -10 19 5
|
||||
10 10 54 51 98 89 43 39 92 83 109 99 26 24 63 58 189 171 12 11 47 43 77 70
|
||||
30 28 70 63 89 79 l35 29 1 138 c1 77 1 139 0 139 0 -1 -53 -47 -117 -103z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,14 +1,21 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"name": "Joyful Fixations",
|
||||
"short_name": "Joyful Fixations",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
"src": "/static/favicons/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/favicons/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
BIN
public/static/favicons/web-app-manifest-192x192.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
public/static/favicons/web-app-manifest-512x512.png
Normal file
After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 5.3 MiB |
Before Width: | Height: | Size: 12 KiB |
BIN
public/static/images/twitter-card-old.png
Normal file
After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |