├── docs ├── src │ ├── env.d.ts │ ├── components │ │ ├── Stats.astro │ │ ├── Features.astro │ │ ├── Card.astro │ │ ├── Header.astro │ │ └── Console.astro │ ├── layouts │ │ └── Layout.astro │ └── pages │ │ └── index.astro ├── tsconfig.json ├── astro.config.mjs ├── package.json ├── tailwind.config.mjs └── public │ └── favicon.svg ├── media ├── demo.gif ├── demo1.3.gif └── diagram.png ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── CODE_OF_CONDUCT.md ├── SECURITY.md ├── workflows │ ├── gitleaks.yml │ ├── greetings.yml │ └── build-and-release.yml └── FUNDING.yml ├── README.md ├── LICENSE ├── .gitignore └── code └── 4cget.go /docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/base" 3 | } 4 | -------------------------------------------------------------------------------- /media/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SegoCode/4cget/HEAD/media/demo.gif -------------------------------------------------------------------------------- /media/demo1.3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SegoCode/4cget/HEAD/media/demo1.3.gif -------------------------------------------------------------------------------- /media/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SegoCode/4cget/HEAD/media/diagram.png -------------------------------------------------------------------------------- /docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import tailwind from '@astrojs/tailwind'; 3 | 4 | export default defineConfig({ 5 | integrations: [tailwind()] 6 | }); -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Contact the developer 4 | url: https://SegoCode.github.io/SegoCode/ 5 | about: To discuss any type of related topic 6 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | This project adheres to **No Code of Conduct**. We are all adults. We accept anyone's contributions. Nothing else matters. 2 | 3 | For more information please visit the [No Code of Conduct](https://github.com/domgetter/NCoC) homepage. 4 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | If you discover a vulnerability in this application, that poses a significant threat to the security of the users, we recommend that you do not open a public issue. Instead, please send your report via [email](https://SegoCode.github.io/SegoCode/). Include as much detailed information as possible to help understand the nature of the vulnerability. 2 | -------------------------------------------------------------------------------- /.github/workflows/gitleaks.yml: -------------------------------------------------------------------------------- 1 | name: Gitleaks 2 | on: [pull_request, push, workflow_dispatch] 3 | jobs: 4 | scan: 5 | name: gitleaks 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout code 9 | uses: actions/checkout@v4 10 | with: 11 | fetch-depth: 0 12 | - name: Run Gitleaks 13 | uses: gitleaks/gitleaks-action@v2 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/basics", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro build", 10 | "preview": "astro preview", 11 | "astro": "astro" 12 | }, 13 | "dependencies": { 14 | "astro": "^4.15.3", 15 | "@astrojs/tailwind": "^5.1.0", 16 | "tailwindcss": "^3.4.1", 17 | "typed.js": "^2.1.0" 18 | } 19 | } -------------------------------------------------------------------------------- /docs/src/components/Stats.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const stats = [ 3 | { number: "", label: "Downloads" }, 4 | { number: "", label: "Satisfaction" }, 5 | { number: "", label: "Support" } 6 | ]; 7 | --- 8 |
9 |
10 | {stats.map(stat => ( 11 |
12 |
{stat.number}
13 |
{stat.label}
14 |
15 | ))} 16 |
17 |
-------------------------------------------------------------------------------- /docs/src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title: string; 4 | } 5 | 6 | const { title } = Astro.props; 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {title} 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], 4 | theme: { 5 | extend: { 6 | animation: { 7 | 'cursor-blink': 'cursor 1s infinite', 8 | 'float': 'float 6s ease-in-out infinite', 9 | }, 10 | keyframes: { 11 | cursor: { 12 | '0%, 100%': { opacity: 1 }, 13 | '50%': { opacity: 0 }, 14 | }, 15 | float: { 16 | '0%, 100%': { transform: 'translateY(0)' }, 17 | '50%': { transform: 'translateY(-20px)' }, 18 | }, 19 | }, 20 | }, 21 | }, 22 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [SegoCode] 2 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 3 | issuehunt: # Replace with a single IssueHunt username 4 | ko_fi: # Replace with a single ko_fi username 5 | liberapay: # Replace with a single Liberapay username 6 | open_collective: # Replace with a single open_collective username 7 | patreon: # Replace with a single Patreon username 8 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 9 | polar: # Replace with a single polar username 10 | buy_me_a_coffee: # Replace with a single buy_me_a_coffee username 11 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 12 | -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request_target, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | steps: 12 | - uses: actions/first-interaction@v1 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | issue-message: "Thank you for your first issue. To better understand your request or the problem you've encountered, please provide as many details as possible. If the behavior changes or if you have new information about your request, don't hesitate to add it. It will be reviewed ASAP." 16 | pr-message: "Thank you for your first pull request to the repository! We're grateful for your contribution and will review it ASAP." 17 | -------------------------------------------------------------------------------- /docs/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro'; 3 | import Header from '../components/Header.astro'; 4 | import Console from '../components/Console.astro'; 5 | import Features from '../components/Features.astro'; 6 | import Stats from '../components/Stats.astro'; 7 | --- 8 | 9 | 10 |
11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 | 20 |
21 |
-------------------------------------------------------------------------------- /docs/src/components/Features.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const features = [ 3 | { 4 | title: "Multi-Threaded", 5 | description: "Lightning-fast with parallel downloads", 6 | icon: "⚡" 7 | }, 8 | { 9 | title: "Zero Dependencies", 10 | description: "Single executable written in raw Golang", 11 | icon: "📦" 12 | }, 13 | { 14 | title: "Proxy Support", 15 | description: "Configurable proxy settings with authentication support", 16 | icon: "🔒" 17 | } 18 | ]; 19 | --- 20 |
21 | {features.map(feature => ( 22 |
23 |
{feature.icon}
24 |

25 | {feature.title} 26 |

27 |

28 | {feature.description} 29 |

30 |
31 | ))} 32 |
-------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this repository 3 | labels: enhancement 4 | title: "[FEATURE REQUEST] - " 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | **Please describe your feature request clearly and concisely** 11 | 12 | - type: textarea 13 | attributes: 14 | label: Feature Description 15 | description: Provide a brief description of the feature you'd like to see implemented 16 | validations: 17 | required: true 18 | 19 | - type: textarea 20 | attributes: 21 | label: Motivation 22 | description: Explain why this feature is important and how it will enhance the user experience 23 | validations: 24 | required: true 25 | 26 | - type: textarea 27 | attributes: 28 | label: Alternatives 29 | description: Discuss any alternative solutions or features you've considered 30 | validations: 31 | required: false 32 | 33 | - type: checkboxes 34 | attributes: 35 | label: Confirmation 36 | options: 37 | - label: I performed a [search of the feature requests](https://github.com/SegoCode/4cget/issues) to avoid suggesting a duplicate feature 38 | required: true 39 | - label: I understand that not filling out this template correctly may lead to the request being closed 40 | required: true 41 | -------------------------------------------------------------------------------- /docs/src/components/Card.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title: string; 4 | body: string; 5 | href: string; 6 | } 7 | 8 | const { href, title, body } = Astro.props; 9 | --- 10 | 11 | 22 | 62 | -------------------------------------------------------------------------------- /docs/src/components/Header.astro: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | 4cget 6 |

7 |
8 |

9 | The fastest 4chan downloader. Multi-threaded, portable, and proxy-ready - all in a single executable with zero dependencies. 10 |

11 | 24 |
25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a bug report to help improve this repository 3 | labels: bug 4 | title: "[BUG REPORT] - " 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | **Please describe your issue clearly and concisely** 11 | 12 | - type: textarea 13 | attributes: 14 | label: Description 15 | description: A brief description of the issue 16 | validations: 17 | required: true 18 | 19 | - type: textarea 20 | attributes: 21 | label: Steps to reproduce 22 | placeholder: | 23 | 1. [First step] 24 | 2. [Second step] 25 | 3. [And so on...] 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | attributes: 31 | label: Expected behavior 32 | description: What you expected to happen 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | attributes: 38 | label: Actual behavior 39 | description: What actually happened 40 | validations: 41 | required: true 42 | 43 | - type: dropdown 44 | attributes: 45 | label: OS version 46 | options: 47 | - Windows 11 48 | - Windows 10 49 | - Windows Other 50 | - Linux Debian 51 | - Linux Arch 52 | - Linux Other 53 | validations: 54 | required: true 55 | 56 | - type: checkboxes 57 | attributes: 58 | label: Confirmation 59 | options: 60 | - label: I performed a [search of the issue tracker](https://github.com/SegoCode/4cget/issues) to avoid opening a duplicate issue 61 | required: true 62 | - label: I understand that not filling out this template correctly may lead to the issue being closed 63 | required: true 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### ⚠️ 07/05/2025 - doesn't work anymore. This repo is no longer maintained, 4chan enhanced its bot protection. ### 2 | 3 | # 4cget 4 | 5 | 6 |

7 | About • 8 | Features • 9 | Quick Start & Information • 10 | Download 11 |

12 | 13 | ## About 14 | Easy to use, simply and fast 4chan thread media downloader. Simple, easy and functional. 15 | 16 | ## Features 17 | 18 | - Portable, single executable 19 | - Configurable proxy 20 | - Customizable monitor mode and intervals 21 | - No dependences, no go mod 22 | 23 | ## Quick Start & Information 24 | 25 |
26 | Thread lifecycle and download process in concurrent image downloading. Click here to show it. 27 |

28 |
29 | 30 | 4cget downloads the files organized by boards and threads. 31 | 32 | ```shell 33 | root 34 | └───board 35 | └───thread 36 | └───files 37 | ``` 38 | 39 | run from source code (Golang installation required). 40 | 41 | ```shell 42 | git clone https://github.com/SegoCode/4cget 43 | cd 4cget\code 44 | go run 4cget.go https://boards.4channel.org/w/thread/... 45 | ``` 46 | Or better [donwload a binary](https://github.com/SegoCode/4cget/releases). 47 | 48 | ### Available Parameters 49 | 50 | `4cget` provides various parameters to customize its behavior. Below are detailed examples and explanations for each available option: 51 | 52 | #### Basic Usage 53 | 54 | Download all images from a thread: 55 | 56 | ```shell 57 | 4cget https://boards.4channel.org/w/thread/... 58 | ``` 59 | 60 | #### Enable Monitor Mode 61 | 62 | Use the `--monitor` flag to enable monitor mode, which checks for new files every specified number of seconds: 63 | 64 | ```shell 65 | 4cget https://boards.4channel.org/w/thread/... --monitor 10 66 | ``` 67 | 68 | *In this example, `4cget` will check every 10 seconds for new images.* 69 | 70 | #### Add Delay Between Downloads 71 | 72 | Use the `--sleep` flag to add a delay between downloads (useful to avoid rate-limiting): 73 | 74 | ```shell 75 | 4cget https://boards.4channel.org/w/thread/... --sleep 2 76 | ``` 77 | 78 | *This adds a 2-second delay between each download.* 79 | 80 | #### Use a Proxy Server 81 | 82 | If you need to route your requests through a proxy server: 83 | 84 | ```shell 85 | 4cget https://boards.4channel.org/w/thread/... --proxy http://proxyserver:port 86 | ``` 87 | 88 | #### Proxy Authentication 89 | 90 | If your proxy server requires authentication: 91 | 92 | ```shell 93 | 4cget https://boards.4channel.org/w/thread/... --proxy http://proxyserver:port --proxyuser username --proxypass password 94 | ``` 95 | 96 | #### Display Help Message 97 | 98 | Use the `--help` flag to display the help message with all available options: 99 | 100 | ```shell 101 | 4cget --help 102 | ``` 103 | 104 | > [!NOTE] 105 | > All flags must be prefixed with `--`. For example, use `--monitor` instead of `-monitor`. 106 | 107 | 108 | ## Download 109 | 110 | https://github.com/SegoCode/4cget/releases/ 111 | 112 | --- 113 |

114 | 115 |

116 | -------------------------------------------------------------------------------- /docs/src/components/Console.astro: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
terminal
9 |
10 |
11 | 19 |
20 |
21 |
22 | 23 | 75 | 76 | 91 | -------------------------------------------------------------------------------- /.github/workflows/build-and-release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build-and-release: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v2 13 | 14 | - name: Set up git 15 | run: | 16 | git config --global user.name "github-actions" 17 | git config --global user.email "github-actions@github.com" 18 | 19 | - name: Fetch all tags 20 | run: git fetch --tags 21 | 22 | - name: Get latest tag 23 | id: get_latest_tag 24 | run: | 25 | # Get the latest tag 26 | latest_tag=$(git describe --tags "$(git rev-list --tags --max-count=1)" 2>/dev/null || echo "") 27 | echo "latest_tag=$latest_tag" >> $GITHUB_ENV 28 | 29 | - name: Determine new version 30 | id: determine_version 31 | run: | 32 | latest_tag=${{ env.latest_tag }} 33 | if [ -z "$latest_tag" ]; then 34 | # Initialize the version to 1.0 if no tags exist 35 | new_version="1.0" 36 | else 37 | # Extract the major and minor version and increment the minor version 38 | major_version=$(echo $latest_tag | cut -d. -f1) 39 | minor_version=$(echo $latest_tag | cut -d. -f2) 40 | new_minor_version=$((minor_version + 1)) 41 | new_version="$major_version.$new_minor_version" 42 | 43 | # Check if the new version tag already exists 44 | while git rev-parse "refs/tags/$new_version" >/dev/null 2>&1; do 45 | new_minor_version=$((new_minor_version + 1)) 46 | new_version="$major_version.$new_minor_version" 47 | done 48 | fi 49 | echo "new_version=$new_version" >> $GITHUB_ENV 50 | 51 | - name: Set up Go 52 | uses: actions/setup-go@v3 53 | with: 54 | go-version: '1.20' 55 | 56 | - name: Compile Go program for multiple platforms 57 | run: | 58 | GOFILE=./code/4cget.go 59 | OUTPUT_DIR=build 60 | 61 | mkdir -p $OUTPUT_DIR 62 | 63 | # Compile for linux-386 64 | echo "Compiling for linux-386..." 65 | GOOS=linux GOARCH=386 go build -trimpath -ldflags="-s -w" -o $OUTPUT_DIR/4cget-linux-386 $GOFILE 66 | 67 | # Compile for linux-amd64 68 | echo "Compiling for linux-amd64..." 69 | GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o $OUTPUT_DIR/4cget-linux-amd64 $GOFILE 70 | 71 | # Compile for linux-arm 72 | echo "Compiling for linux-arm..." 73 | GOOS=linux GOARCH=arm go build -trimpath -ldflags="-s -w" -o $OUTPUT_DIR/4cget-linux-arm $GOFILE 74 | 75 | # Compile for windows-386.exe 76 | echo "Compiling for windows-386.exe..." 77 | GOOS=windows GOARCH=386 go build -trimpath -ldflags="-s -w" -o $OUTPUT_DIR/4cget-windows-386.exe $GOFILE 78 | 79 | # Compile for windows-amd64.exe 80 | echo "Compiling for windows-amd64.exe..." 81 | GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o $OUTPUT_DIR/4cget-windows-amd64.exe $GOFILE 82 | 83 | - name: Create new tag 84 | env: 85 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 86 | run: | 87 | new_version=${{ env.new_version }} 88 | git tag -a "$new_version" -m "Automatically generated version $new_version" 89 | git push origin "$new_version" 90 | 91 | - name: Create Release and Upload Assets 92 | uses: softprops/action-gh-release@v1 93 | with: 94 | files: build/* 95 | tag_name: ${{ env.new_version }} 96 | name: 4cget 97 | draft: false 98 | prerelease: false 99 | env: 100 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) SegoCode 2 | All rights reserved. 3 | 4 | Section 1 - Definitions 5 | 6 | 1.1 "NonCommercial" pertains to any use, distribution, or modification of the 7 | licensed material that does not primarily aim to achieve commercial 8 | advantage or generate monetary compensation. This includes, but is not 9 | limited to, activities such as distributing the licensed material as part 10 | of an application or product that is sold, using the licensed material in 11 | advertising, and creating products or services with the licensed material 12 | that are subsequently sold. 13 | 14 | 1.2 "Adapted Material" refers to any work derived from or based upon the licensed 15 | material. This includes, but is not limited to, translations, alterations, 16 | rearrangements, transformations, source code modifications, compiled code 17 | alterations, architectural redesigns, or the integration of the licensed 18 | material into other software projects. 19 | 20 | 1.3 "NonAdapted Material" refers to exact copies of the licensed material, either 21 | in source code or binary form, which are reproduced without any changes, 22 | modifications, or transformations. 23 | 24 | Section 2 - License Conditions 25 | 26 | 2.1 Distribution and usage in source or binary forms are permitted solely for 27 | NonCommercial purposes for both NonAdapted Material and Adapted Material 28 | excluding NonAdapted Material cases in 2.4 section. 29 | 30 | 2.2 Redistributions for any NonAdapted Material or Adapted Material, either 31 | in source code or binary form, must include the original copyright notice, 32 | this license, the disclaimer, and comply with the requirements specified 33 | herein. For binary form redistributions, these documents must be included 34 | in any provided documentation or materials or a clearly accessible link 35 | to this license ensuring that recipients can easily review the license terms. 36 | 37 | 2.3 For any Adapted Material, whether in source code or binary form, and for any 38 | instance of the licensed material utilized in applications accessible over 39 | the internet that run on a server, the source code must be made accessible 40 | through a public repository, a downloadable archive, or an equivalent 41 | method, ensuring that users have the capability to access, review, and 42 | download the code, and must clearly credit the original work and author. 43 | 44 | 2.4 For any NonAdapted Material that are offered on download sites, marketplaces, or 45 | software distribution platforms, are permitted under the condition that any 46 | economic benefit derived from such distribution is strictly indirect, including 47 | but not limited to advertisements or link redirectors. Direct sales or charges 48 | for access to the licensed material are not permitted under this license. Upon 49 | the original author distributing the material on the same platform, all alternative 50 | downloads and any associated revenue-generating mechanisms must be discontinued 51 | immediately. Acceptance of this license constitutes agreement that the copyright 52 | holder may request the immediate cessation of such downloads and activities on 53 | such platforms, and the distributor must comply with such request without delay. 54 | 55 | 2.5 Distributing the licensed material alongside unrelated or harmful software, such as adware, 56 | malware, or spyware or implying endorsement or association with the original author 57 | or recognized entities without permission, is prohibited. 58 | 59 | Section 3 - Updates and Revisions 60 | 61 | 3.1 The copyright holder retains the right to amend, update, or otherwise modify this 62 | license at any time without prior notice. Users should periodically review the 63 | license terms to stay informed of any changes. 64 | 65 | 3.2 Continued use of the licensed material after such changes signifies acceptance of the 66 | revised license terms. It is the user's responsibility to ensure ongoing compliance 67 | with the most current version of the license. 68 | 69 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 70 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 71 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 72 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 73 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 74 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 75 | SOFTWARE. 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/git,gpg,ssh,vim,linux,macos,windows,notepadpp,sublimetext,intellij+all,visualstudiocode 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=git,gpg,ssh,vim,linux,macos,windows,notepadpp,sublimetext,intellij+all,visualstudiocode 3 | 4 | ### Git ### 5 | # Created by git for backups. To disable backups in Git: 6 | # $ git config --global mergetool.keepBackup false 7 | *.orig 8 | 9 | # Created by git when using merge tools for conflicts 10 | *.BACKUP.* 11 | *.BASE.* 12 | *.LOCAL.* 13 | *.REMOTE.* 14 | *_BACKUP_*.txt 15 | *_BASE_*.txt 16 | *_LOCAL_*.txt 17 | *_REMOTE_*.txt 18 | 19 | ### GPG ### 20 | secring.* 21 | 22 | 23 | ### Intellij+all ### 24 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 25 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 26 | 27 | # User-specific stuff 28 | .idea/**/workspace.xml 29 | .idea/**/tasks.xml 30 | .idea/**/usage.statistics.xml 31 | .idea/**/dictionaries 32 | .idea/**/shelf 33 | 34 | # AWS User-specific 35 | .idea/**/aws.xml 36 | 37 | # Generated files 38 | .idea/**/contentModel.xml 39 | 40 | # Sensitive or high-churn files 41 | .idea/**/dataSources/ 42 | .idea/**/dataSources.ids 43 | .idea/**/dataSources.local.xml 44 | .idea/**/sqlDataSources.xml 45 | .idea/**/dynamic.xml 46 | .idea/**/uiDesigner.xml 47 | .idea/**/dbnavigator.xml 48 | 49 | # Gradle 50 | .idea/**/gradle.xml 51 | .idea/**/libraries 52 | 53 | # Gradle and Maven with auto-import 54 | # When using Gradle or Maven with auto-import, you should exclude module files, 55 | # since they will be recreated, and may cause churn. Uncomment if using 56 | # auto-import. 57 | # .idea/artifacts 58 | # .idea/compiler.xml 59 | # .idea/jarRepositories.xml 60 | # .idea/modules.xml 61 | # .idea/*.iml 62 | # .idea/modules 63 | # *.iml 64 | # *.ipr 65 | 66 | # CMake 67 | cmake-build-*/ 68 | 69 | # Mongo Explorer plugin 70 | .idea/**/mongoSettings.xml 71 | 72 | # File-based project format 73 | *.iws 74 | 75 | # IntelliJ 76 | out/ 77 | 78 | # mpeltonen/sbt-idea plugin 79 | .idea_modules/ 80 | 81 | # JIRA plugin 82 | atlassian-ide-plugin.xml 83 | 84 | # Cursive Clojure plugin 85 | .idea/replstate.xml 86 | 87 | # SonarLint plugin 88 | .idea/sonarlint/ 89 | 90 | # Crashlytics plugin (for Android Studio and IntelliJ) 91 | com_crashlytics_export_strings.xml 92 | crashlytics.properties 93 | crashlytics-build.properties 94 | fabric.properties 95 | 96 | # Editor-based Rest Client 97 | .idea/httpRequests 98 | 99 | # Android studio 3.1+ serialized cache file 100 | .idea/caches/build_file_checksums.ser 101 | 102 | ### Intellij+all Patch ### 103 | # Ignore everything but code style settings and run configurations 104 | # that are supposed to be shared within teams. 105 | 106 | .idea/* 107 | 108 | !.idea/codeStyles 109 | !.idea/runConfigurations 110 | 111 | ### Linux ### 112 | *~ 113 | 114 | # temporary files which can be created if a process still has a handle open of a deleted file 115 | .fuse_hidden* 116 | 117 | # KDE directory preferences 118 | .directory 119 | 120 | # Linux trash folder which might appear on any partition or disk 121 | .Trash-* 122 | 123 | # .nfs files are created when an open file is removed but is still being accessed 124 | .nfs* 125 | 126 | ### macOS ### 127 | # General 128 | .DS_Store 129 | .AppleDouble 130 | .LSOverride 131 | 132 | # Icon must end with two \r 133 | Icon 134 | 135 | 136 | # Thumbnails 137 | ._* 138 | 139 | # Files that might appear in the root of a volume 140 | .DocumentRevisions-V100 141 | .fseventsd 142 | .Spotlight-V100 143 | .TemporaryItems 144 | .Trashes 145 | .VolumeIcon.icns 146 | .com.apple.timemachine.donotpresent 147 | 148 | # Directories potentially created on remote AFP share 149 | .AppleDB 150 | .AppleDesktop 151 | Network Trash Folder 152 | Temporary Items 153 | .apdisk 154 | 155 | ### macOS Patch ### 156 | # iCloud generated files 157 | *.icloud 158 | 159 | ### NotepadPP ### 160 | # Notepad++ backups # 161 | *.bak 162 | 163 | ### SSH ### 164 | **/.ssh/id_* 165 | **/.ssh/*_id_* 166 | **/.ssh/known_hosts 167 | 168 | ### SublimeText ### 169 | # Cache files for Sublime Text 170 | *.tmlanguage.cache 171 | *.tmPreferences.cache 172 | *.stTheme.cache 173 | 174 | # Workspace files are user-specific 175 | *.sublime-workspace 176 | 177 | # Project files should be checked into the repository, unless a significant 178 | # proportion of contributors will probably not be using Sublime Text 179 | # *.sublime-project 180 | 181 | # SFTP configuration file 182 | sftp-config.json 183 | sftp-config-alt*.json 184 | 185 | # Package control specific files 186 | Package Control.last-run 187 | Package Control.ca-list 188 | Package Control.ca-bundle 189 | Package Control.system-ca-bundle 190 | Package Control.cache/ 191 | Package Control.ca-certs/ 192 | Package Control.merged-ca-bundle 193 | Package Control.user-ca-bundle 194 | oscrypto-ca-bundle.crt 195 | bh_unicode_properties.cache 196 | 197 | # Sublime-github package stores a github token in this file 198 | # https://packagecontrol.io/packages/sublime-github 199 | GitHub.sublime-settings 200 | 201 | ### Vim ### 202 | # Swap 203 | [._]*.s[a-v][a-z] 204 | !*.svg # comment out if you don't need vector files 205 | [._]*.sw[a-p] 206 | [._]s[a-rt-v][a-z] 207 | [._]ss[a-gi-z] 208 | [._]sw[a-p] 209 | 210 | # Session 211 | Session.vim 212 | Sessionx.vim 213 | 214 | # Temporary 215 | .netrwhist 216 | # Auto-generated tag files 217 | tags 218 | # Persistent undo 219 | [._]*.un~ 220 | 221 | ### VisualStudioCode ### 222 | .vscode/* 223 | !.vscode/settings.json 224 | !.vscode/tasks.json 225 | !.vscode/launch.json 226 | !.vscode/extensions.json 227 | !.vscode/*.code-snippets 228 | 229 | # Local History for Visual Studio Code 230 | .history/ 231 | 232 | # Built Visual Studio Code Extensions 233 | *.vsix 234 | 235 | ### VisualStudioCode Patch ### 236 | # Ignore all local history of files 237 | .history 238 | .ionide 239 | 240 | ### Windows ### 241 | # Windows thumbnail cache files 242 | Thumbs.db 243 | Thumbs.db:encryptable 244 | ehthumbs.db 245 | ehthumbs_vista.db 246 | 247 | # Dump file 248 | *.stackdump 249 | 250 | # Folder config file 251 | [Dd]esktop.ini 252 | 253 | # Recycle Bin used on file shares 254 | $RECYCLE.BIN/ 255 | 256 | # Windows Installer files 257 | *.cab 258 | *.msi 259 | *.msix 260 | *.msm 261 | *.msp 262 | 263 | # Windows shortcuts 264 | *.lnk 265 | 266 | # End of https://www.toptal.com/developers/gitignore/api/git,gpg,ssh,vim,linux,macos,windows,notepadpp,sublimetext,intellij+all,visualstudiocode 267 | code/compile.bat 268 | -------------------------------------------------------------------------------- /code/4cget.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "math" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "regexp" 14 | "strings" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | const version = "1.7" // Current version 20 | 21 | var monitorMode bool 22 | 23 | // SiteInfo holds the URL pattern, regex for image extraction, and an ID. 24 | type SiteInfo struct { 25 | ID string 26 | URL string 27 | ImgRE *regexp.Regexp 28 | } 29 | 30 | // Initialize the site info map with URL patterns and corresponding regex. 31 | var siteInfoMap = map[string]SiteInfo{ 32 | "4chan": { 33 | ID: "4chan", 34 | URL: "https://boards.4chan.org", 35 | ImgRE: regexp.MustCompile(`]+href="(//i\.4cdn\.org[^"]+)"`), 36 | }, 37 | "twochen": { 38 | ID: "twochen", 39 | URL: "https://sturdychan.help/", 40 | ImgRE: regexp.MustCompile(`(https?://[^/]+/assets/images/src/[a-zA-Z0-9]+\.(?:png|jpg))`), 41 | }, 42 | } 43 | 44 | // findImages extracts image URLs from the given HTML based on the site specified. 45 | func findImages(html, siteID string) []string { 46 | var out []string 47 | siteInfo, exists := siteInfoMap[siteID] 48 | if !exists { 49 | fmt.Printf("No site information found for ID: %s\n", siteID) 50 | return out 51 | } 52 | 53 | matches := siteInfo.ImgRE.FindAllStringSubmatch(html, -1) 54 | for _, match := range matches { 55 | url := match[1] 56 | if siteID == siteInfoMap["4chan"].ID { 57 | url = strings.Replace(url, "//i.4cdn.org", "https://i.4cdn.org", 1) 58 | } 59 | out = append(out, url) 60 | } 61 | 62 | uniqueOut := unique(out) // Clear array of duplicates 63 | return uniqueOut 64 | } 65 | 66 | // unique removes duplicate strings from a slice. 67 | func unique(input []string) []string { 68 | u := make(map[string]bool) 69 | var uniqueList []string 70 | for _, val := range input { 71 | if _, ok := u[val]; !ok { 72 | u[val] = true 73 | uniqueList = append(uniqueList, val) 74 | } 75 | } 76 | return uniqueList 77 | } 78 | 79 | func downloadFile(wg *sync.WaitGroup, url string, fileName string, path string, client *http.Client) { 80 | defer wg.Done() 81 | 82 | resp, err := client.Get(url) 83 | if err != nil { 84 | fmt.Println("[!] Error downloading file:", err) 85 | return 86 | } 87 | defer resp.Body.Close() 88 | 89 | if resp.StatusCode == 429 { 90 | fmt.Println("[!] Received HTTP 429 Too Many Requests. You are being rate-limited.") 91 | fmt.Println("[!] Consider using the --sleep flag to add delays between downloads.") 92 | return 93 | } 94 | 95 | if resp.StatusCode != 404 && resp.StatusCode == 200 { 96 | filePath := path + "/" + fileName 97 | if _, err := os.Stat(filePath); os.IsNotExist(err) || !monitorMode { 98 | img, err := os.Create(filePath) 99 | if err != nil { 100 | fmt.Println("[!] Error creating file:", err) 101 | return 102 | } 103 | defer img.Close() 104 | 105 | b, err := io.Copy(img, resp.Body) 106 | if err != nil { 107 | fmt.Println("[!] Error copying response body:", err) 108 | return 109 | } 110 | 111 | suffixes := []string{"B", "KB", "MB", "GB", "TB"} 112 | 113 | base := math.Log(float64(b)) / math.Log(1024) 114 | getSize := math.Pow(1024, base-math.Floor(base)) 115 | getSuffix := suffixes[int(math.Floor(base))] 116 | 117 | fmt.Printf("File downloaded: %s - Size: %.2f %s\n", fileName, getSize, getSuffix) 118 | } 119 | } else { 120 | fmt.Printf("[!] Received HTTP %d for %s\n", resp.StatusCode, url) 121 | } 122 | } 123 | 124 | // checkForUpdates checks the latest release from GitHub and compares it with the current version. 125 | func checkForUpdates() (latestVersion string, updateAvailable bool) { 126 | apiURL := "https://api.github.com/repos/SegoCode/4cget/releases/latest" 127 | resp, err := http.Get(apiURL) 128 | if err != nil { 129 | fmt.Println("[!] Error checking for updates:", err) 130 | return "", false 131 | } 132 | defer resp.Body.Close() 133 | 134 | if resp.StatusCode != 200 { 135 | fmt.Printf("[!] GitHub API returned status code %d\n", resp.StatusCode) 136 | return "", false 137 | } 138 | 139 | var release struct { 140 | TagName string `json:"tag_name"` 141 | } 142 | if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { 143 | fmt.Println("[!] Error decoding GitHub API response:", err) 144 | return "", false 145 | } 146 | 147 | latestVersion = strings.TrimPrefix(release.TagName, "v") 148 | if latestVersion != version { 149 | return latestVersion, true 150 | } 151 | return latestVersion, false 152 | } 153 | 154 | // displayHelp shows the help message with explanations and examples. 155 | func displayHelp() { 156 | fmt.Println(` 157 | 4cget - A tool to download images from 4chan threads. 158 | 159 | Usage: 160 | 4cget [options] 161 | 162 | Options: 163 | --help Display this help message. 164 | --monitor Enable monitor mode with interval in seconds. 165 | The program will check for new images every specified interval. 166 | --sleep Sleep duration in seconds between downloads. 167 | Useful to avoid getting rate-limited by the server. 168 | --proxy Proxy URL (e.g., http://proxyserver:port). 169 | --proxyuser Proxy username for authentication. 170 | --proxypass Proxy password for authentication. 171 | 172 | Examples: 173 | 174 | Basic usage: 175 | 4cget https://boards.4chan.org/w/thread/123456 176 | 177 | Enable monitor mode with a 60-second interval: 178 | 4cget --monitor 60 https://boards.4chan.org/w/thread/123456 179 | 180 | Use a proxy with authentication: 181 | 4cget --proxy http://proxyserver:port --proxyuser username --proxypass password https://boards.4chan.org/w/thread/123456 182 | 183 | Add delay between downloads to prevent rate-limiting: 184 | 4cget --sleep 2 https://boards.4chan.org/w/thread/123456 185 | 186 | Note: 187 | - Ensure that all flags are prefixed with '--'. 188 | - The thread URL must be a valid URL from a supported site. 189 | - Use '--sleep' to add delays between downloads to avoid getting rate-limited (HTTP 429 errors). 190 | `) 191 | } 192 | 193 | func main() { 194 | var wg sync.WaitGroup 195 | var inputUrl string 196 | var thread string 197 | var siteID string 198 | 199 | // Define command-line flags 200 | fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 201 | helpFlag := fs.Bool("help", false, "Display this help message") 202 | monitorIntervalFlag := fs.Int("monitor", 0, "Enable monitor mode with interval in seconds") 203 | sleepFlag := fs.Int("sleep", 0, "Sleep duration in seconds between downloads") 204 | proxyFlag := fs.String("proxy", "", "Proxy URL (e.g., http://proxyserver:port)") 205 | proxyUserFlag := fs.String("proxyuser", "", "Proxy username") 206 | proxyPassFlag := fs.String("proxypass", "", "Proxy password") 207 | 208 | // Manually parse flags and positional arguments 209 | var args []string 210 | for i := 1; i < len(os.Args); i++ { 211 | arg := os.Args[i] 212 | if arg == "--" { 213 | // All remaining args are positional 214 | args = append(args, os.Args[i+1:]...) 215 | break 216 | } 217 | if strings.HasPrefix(arg, "--") { 218 | // Flag 219 | fs.Parse(os.Args[i:]) 220 | break 221 | } 222 | if strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") { 223 | fmt.Printf("Invalid flag: %s. Flags must start with '--'.\n", arg) 224 | os.Exit(1) 225 | } 226 | // Positional argument 227 | args = append(args, arg) 228 | } 229 | 230 | // After parsing flags, any remaining arguments are positional 231 | args = append(args, fs.Args()...) 232 | 233 | // If --help is provided, display help message and exit 234 | if *helpFlag { 235 | displayHelp() 236 | return 237 | } 238 | 239 | // Input URL validation 240 | if len(args) < 1 { 241 | fmt.Println("[!] USAGE: 4cget [options] ") 242 | fmt.Println("Use '--help' to see available options.") 243 | os.Exit(1) 244 | } 245 | inputUrl = args[0] 246 | 247 | monitorMode = (*monitorIntervalFlag > 0) 248 | secondsIteration := *monitorIntervalFlag 249 | sleepDuration := *sleepFlag 250 | proxyURL := *proxyFlag 251 | 252 | parsedURL, errParse := url.ParseRequestURI(inputUrl) 253 | if errParse != nil { 254 | fmt.Println("[!] URL NOT VALID (Example: https://boards.4channel.org/w/thread/.../...)") 255 | os.Exit(1) 256 | } 257 | 258 | for _, site := range siteInfoMap { 259 | parsedSiteURL, err := url.Parse(site.URL) 260 | if err != nil { 261 | fmt.Printf("Error parsing site URL %s: %v\n", site.URL, err) 262 | continue 263 | } 264 | if parsedURL.Host == parsedSiteURL.Host { 265 | siteID = site.ID 266 | break 267 | } 268 | } 269 | 270 | if siteID == "" { 271 | fmt.Println("[!] Unsupported site") 272 | os.Exit(1) 273 | } 274 | 275 | fmt.Println(` 276 | ░░██╗██╗░█████╗░░██████╗░███████╗████████╗ 277 | ░██╔╝██║██╔══██╗██╔════╝░██╔════╝╚══██╔══╝ 278 | ██╔╝░██║██║░░╚═╝██║░░██╗░█████╗░░░░░██║░░░ 279 | ███████║██║░░██╗██║░░╚██╗██╔══╝░░░░░██║░░░ 280 | ╚════██║╚█████╔╝╚██████╔╝███████╗░░░██║░░░ 281 | ░░░░░╚═╝░╚════╝░░╚═════╝░╚══════╝░░░╚═╝░░░ 282 | [ github.com/SegoCode ]` + "\n") 283 | 284 | // Check for updates before starting the download 285 | latestVersion, updateAvailable := checkForUpdates() 286 | if updateAvailable { 287 | fmt.Printf("[*] UPDATE AVAILABLE %s [*]\n\n", latestVersion) 288 | } 289 | 290 | fmt.Println("[*] DOWNLOAD STARTED (" + inputUrl + ") [*]\n") 291 | if monitorMode { 292 | fmt.Println("[*] MONITOR MODE ENABLED [*]\n") 293 | } 294 | 295 | start := time.Now() 296 | files := 0 297 | 298 | // Parse board and thread from URL 299 | parts := strings.Split(inputUrl, "/") 300 | board := parts[3] 301 | 302 | // Handle the thread part depending on the site 303 | if siteID == siteInfoMap["4chan"].ID { 304 | thread = parts[5] 305 | } else { 306 | thread = parts[4] 307 | } 308 | 309 | // Create necessary directories 310 | actualPath, _ := os.Getwd() 311 | os.MkdirAll(fmt.Sprintf("%s/%s", actualPath, board), os.ModePerm) 312 | os.MkdirAll(fmt.Sprintf("%s/%s/%s", actualPath, board, thread), os.ModePerm) 313 | pathResult := fmt.Sprintf("%s/%s/%s", actualPath, board, thread) 314 | 315 | fmt.Println("Folder created : " + actualPath + "...\n") 316 | 317 | // Setup HTTP client with optional proxy and authentication 318 | client := &http.Client{} 319 | if proxyURL != "" { 320 | proxyParsed, err := url.Parse(proxyURL) 321 | if err != nil { 322 | fmt.Println("[!] Invalid proxy URL:", err) 323 | os.Exit(1) 324 | } 325 | if *proxyUserFlag != "" { 326 | proxyParsed.User = url.UserPassword(*proxyUserFlag, *proxyPassFlag) 327 | } 328 | client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyParsed)} 329 | } 330 | 331 | for { // Main loop for monitorMode 332 | resp, err := client.Get(inputUrl) 333 | if err != nil { 334 | fmt.Println("[!] Error fetching URL:", err) 335 | os.Exit(1) 336 | } 337 | body, _ := ioutil.ReadAll(resp.Body) 338 | resp.Body.Close() 339 | imageURLs := findImages(string(body), siteID) 340 | for _, each := range imageURLs { 341 | parts := strings.Split(each, "/") 342 | nameImg := parts[len(parts)-1] 343 | wg.Add(1) 344 | go downloadFile(&wg, each, nameImg, pathResult, client) 345 | files++ 346 | 347 | // Sleep between starting downloads if sleepDuration > 0 348 | if sleepDuration > 0 { 349 | time.Sleep(time.Duration(sleepDuration) * time.Second) 350 | } 351 | } 352 | wg.Wait() 353 | if !monitorMode { 354 | break // Exit main loop 355 | } else { 356 | for i := secondsIteration; i >= 0; i-- { 357 | fmt.Printf("Press Ctrl+C to close 4cget\n") 358 | fmt.Printf("Checking for new files in %v seconds....\n", i) 359 | time.Sleep(1 * time.Second) 360 | print("\033[F\033[F") 361 | } 362 | } 363 | } 364 | 365 | fmt.Printf("\n✓ DOWNLOAD COMPLETE, %v FILES IN %v\n", files, time.Since(start)) 366 | } 367 | --------------------------------------------------------------------------------