Compare commits
23 Commits
c6062426e3
...
5f319e39b7
Author | SHA1 | Date | |
---|---|---|---|
5f319e39b7 | |||
f47646c66b | |||
b46ecacbd9 | |||
981a11ce83 | |||
6b26b89c69 | |||
a3dd6698fc | |||
87ab654f55 | |||
25f10a8c50 | |||
f8f7832dd7 | |||
539b75cb09 | |||
63e67772a3 | |||
27b05b6184 | |||
03a6d9b54f | |||
7002439532 | |||
69db2dbbee | |||
7bc1157ffb | |||
5c6609ff0a | |||
03e35c18a9 | |||
a93a88d344 | |||
16794bf488 | |||
dce1b2f9a5 | |||
746b40fa18 | |||
e85b021383 |
406
.gitignore
vendored
406
.gitignore
vendored
@ -1,5 +1,5 @@
|
|||||||
# Created by https://www.toptal.com/developers/gitignore/api/python
|
### Build ###
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
app_version_file.txt
|
||||||
|
|
||||||
### Python ###
|
### Python ###
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
@ -13,6 +13,7 @@ __pycache__/
|
|||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
build/
|
build/
|
||||||
|
output/
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
downloads/
|
downloads/
|
||||||
@ -163,4 +164,403 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/python
|
### VisualStudio ###
|
||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
##
|
||||||
|
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.rsuser
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Mono auto generated files
|
||||||
|
mono_crash.*
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
[Ww][Ii][Nn]32/
|
||||||
|
[Aa][Rr][Mm]/
|
||||||
|
[Aa][Rr][Mm]64/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Ll]og/
|
||||||
|
[Ll]ogs/
|
||||||
|
|
||||||
|
# Visual Studio 2015/2017 cache/options directory
|
||||||
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# Visual Studio 2017 auto generated files
|
||||||
|
Generated\ Files/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUnit
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
nunit-*.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
# Benchmark Results
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# ASP.NET Scaffolding
|
||||||
|
ScaffoldingReadMe.txt
|
||||||
|
|
||||||
|
# StyleCop
|
||||||
|
StyleCopReport.xml
|
||||||
|
|
||||||
|
# Files built by Visual Studio
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_h.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.iobj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.ipdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*_wpftmp.csproj
|
||||||
|
*.tlog
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.VC.opendb
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# Visual Studio Trace Files
|
||||||
|
*.e2e
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# AxoCover is a Code Coverage Tool
|
||||||
|
.axoCover/*
|
||||||
|
!.axoCover/settings.json
|
||||||
|
|
||||||
|
# Coverlet is a free, cross platform Code Coverage Tool
|
||||||
|
coverage*.json
|
||||||
|
coverage*.xml
|
||||||
|
coverage*.info
|
||||||
|
|
||||||
|
# Visual Studio code coverage results
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
# in these scripts will be unencrypted
|
||||||
|
PublishScripts/
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# NuGet Symbol Packages
|
||||||
|
*.snupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/[Pp]ackages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/[Pp]ackages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/[Pp]ackages/repositories.config
|
||||||
|
# NuGet v3's project.json files produces more ignorable files
|
||||||
|
*.nuget.props
|
||||||
|
*.nuget.targets
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Windows Store app package directories and files
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
Package.StoreAssociation.xml
|
||||||
|
_pkginfo.txt
|
||||||
|
*.appx
|
||||||
|
*.appxbundle
|
||||||
|
*.appxupload
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!?*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
# Including strong name files can present a security risk
|
||||||
|
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||||
|
#*.snk
|
||||||
|
|
||||||
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
|
#bower_components/
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
ServiceFabricBackup/
|
||||||
|
*.rptproj.bak
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.ndf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
*.rptproj.rsuser
|
||||||
|
*- [Bb]ackup.rdl
|
||||||
|
*- [Bb]ackup ([0-9]).rdl
|
||||||
|
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||||
|
*.vbw
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||||
|
*.vbp
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||||
|
*.dsw
|
||||||
|
*.dsp
|
||||||
|
|
||||||
|
# Visual Studio 6 technical files
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
# CodeRush personal settings
|
||||||
|
.cr/personal
|
||||||
|
|
||||||
|
# Python Tools for Visual Studio (PTVS)
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Cake - Uncomment if you are using it
|
||||||
|
# tools/**
|
||||||
|
# !tools/packages.config
|
||||||
|
|
||||||
|
# Tabs Studio
|
||||||
|
*.tss
|
||||||
|
|
||||||
|
# Telerik's JustMock configuration file
|
||||||
|
*.jmconfig
|
||||||
|
|
||||||
|
# BizTalk build output
|
||||||
|
*.btp.cs
|
||||||
|
*.btm.cs
|
||||||
|
*.odx.cs
|
||||||
|
*.xsd.cs
|
||||||
|
|
||||||
|
# OpenCover UI analysis results
|
||||||
|
OpenCover/
|
||||||
|
|
||||||
|
# Azure Stream Analytics local run output
|
||||||
|
ASALocalRun/
|
||||||
|
|
||||||
|
# MSBuild Binary and Structured Log
|
||||||
|
*.binlog
|
||||||
|
|
||||||
|
# NVidia Nsight GPU debugger configuration file
|
||||||
|
*.nvuser
|
||||||
|
|
||||||
|
# MFractors (Xamarin productivity tool) working folder
|
||||||
|
.mfractor/
|
||||||
|
|
||||||
|
# Local History for Visual Studio
|
||||||
|
.localhistory/
|
||||||
|
|
||||||
|
# Visual Studio History (VSHistory) files
|
||||||
|
.vshistory/
|
||||||
|
|
||||||
|
# BeatPulse healthcheck temp database
|
||||||
|
healthchecksdb
|
||||||
|
|
||||||
|
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||||
|
MigrationBackup/
|
||||||
|
|
||||||
|
# Ionide (cross platform F# VS Code tools) working folder
|
||||||
|
.ionide/
|
||||||
|
|
||||||
|
# Fody - auto-generated XML schema
|
||||||
|
FodyWeavers.xsd
|
||||||
|
|
||||||
|
# VS Code files for those working on multiple tools
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Windows Installer files from build outputs
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
# JetBrains Rider
|
||||||
|
*.sln.iml
|
||||||
|
|
||||||
|
### VisualStudio Patch ###
|
||||||
|
# Additional files built by Visual Studio
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/python,visualstudio
|
108
README.md
Normal file
108
README.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# WebPicDownloader
|
||||||
|
|
||||||
|
[![Donate][link-icon-coffee]][link-paypal-me] [![Website][link-icon-website]][link-website]
|
||||||
|
|
||||||
|
[link-icon-coffee]: https://img.shields.io/badge/%E2%98%95-Buy%20me%20a%20cup%20of%20coffee-991481.svg
|
||||||
|
[link-paypal-me]: https://www.paypal.me/EndMove/2.5eur
|
||||||
|
[link-icon-website]: https://img.shields.io/badge/%F0%9F%92%BB-My%20Web%20Site-0078D4.svg
|
||||||
|
[link-website]: https://www.endmove.eu/
|
||||||
|
|
||||||
|
## What is webpicdownloader ?
|
||||||
|
|
||||||
|
WebPicDownloader is a scraping tool that allows you to download all the images of a website. Basically WebPic is a Python script around which a graphical interface has been added to make it easier to use.
|
||||||
|
|
||||||
|
You will find [here](#windows-application) utility information to use the Windows application `WebPicDownloader.exe`. And [here](#use-python-script) information to use or implement the Python script `WebPicDownloader.py` in your application (without the graphical interface).
|
||||||
|
|
||||||
|
## Windows application
|
||||||
|
|
||||||
|
To use WebPic on windows nothing more simple, download the executable `.exe` of the last [release here](https://git.endmove.eu/EndMove/WebPicDownloader/releases) (be careful to download the latest release and not a pre-release).
|
||||||
|
|
||||||
|
Execute the file ``WebPicDownloader.exe`` and enjoy it! 👌
|
||||||
|
|
||||||
|
## Use Python script
|
||||||
|
|
||||||
|
To start, find the script to use or to add to your code [here](webpicdownloader/model/WebPicDownloader.py).
|
||||||
|
|
||||||
|
### CLI Run Requirements
|
||||||
|
|
||||||
|
To use the script check the following prerequisites.
|
||||||
|
|
||||||
|
* Python `>= 3.10.6` ;
|
||||||
|
* beautifulsoup4 `>= 4.11.1` ;
|
||||||
|
* bs4 (BeautifulSoup) `>= 0.0.1` ;
|
||||||
|
* urllib3 `>= 1.26.12` ;
|
||||||
|
|
||||||
|
### Console Use ?
|
||||||
|
|
||||||
|
If you just want to use the console version of the script without the built-in GUI then you just need to check the [prerequisites](#cli-run-requirements) and run the script as follows:
|
||||||
|
|
||||||
|
```python
|
||||||
|
python3 WebPicDownloader.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integrate to your code ?
|
||||||
|
|
||||||
|
First of all you have to know that WebPicDownloader has a deamon worker that downloads all the images asynchronously (this allows you not to block your program when a download is in progress). This same worker will be automatically killed as soon as your program finishes. WebPicDownloader therefore provides a blocking stop function allowing you to wait for the end of the download. See the information below. The prerequisites are the same as if you were running the script from the command line, see [prerequisites](#cli-run-requirements).
|
||||||
|
|
||||||
|
#### Step 1
|
||||||
|
|
||||||
|
Instantiate your WebPicDownloader object like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from WebPicDownloader import WebPicDownlodaer, MessageType
|
||||||
|
|
||||||
|
webpic = WebPicDownloader()
|
||||||
|
```
|
||||||
|
|
||||||
|
The constructor can take several parameters (`path: str, headers: dict, messenger, success, failure`) (see the documentation).
|
||||||
|
|
||||||
|
#### Step 2
|
||||||
|
|
||||||
|
Define the WebPicDownloader callback functions. There are 3 main ones, the first (messenger callback) will be called at each system event and takes the following parameters (`message: str, type: MessageType`). The second (success callback) will be called at the end of processing if no major errors occur, it takes the following parameters (`message: str`). The third and last function (failure callback) will be called if a major error occurs or the download fails, it takes the following parameter (`message: str`).
|
||||||
|
|
||||||
|
By default, these functions print their results with a simple `print(message)` in the console. In case you implement WebPicDownloader in a graphical program, you should by convention remove all printing from your application and therefore define your own callback functions for WebpicDownloder. Below is an example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from WebPicDownloader import WebPicDownlodaer, MessageType
|
||||||
|
|
||||||
|
# Consider instantiating before the main loop of your program is launched.
|
||||||
|
webpic = WebPicDownloader()
|
||||||
|
|
||||||
|
# Pay attention to the signature of the functions
|
||||||
|
webpic.set_success_callback(lambda message: print(f"Success ! [{message}]."))
|
||||||
|
webpic.set_failure_callback(lambda message: print(f"Success ! [{message}]."))
|
||||||
|
webpic.set_messenger_callback(lambda message, msg_type: print(f"[{msg_type}]: {message}."))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3
|
||||||
|
|
||||||
|
Once WebPicDownloader instantiated and the callback functions configured, we have to launch the download and stop it. It is important to know that the script does not have a function to stop a download in progress, in fact the stop function will allow you to wait for the end of the download and then turn off the program or to kill the worker automatically when the main thread dies.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from time import sleep
|
||||||
|
from WebPicDownloader import WebPicDownlodaer, MessageType
|
||||||
|
|
||||||
|
webpic = WebPicDownloader()
|
||||||
|
|
||||||
|
# ... callbacks ...
|
||||||
|
|
||||||
|
# Webpic will give the task to its worker and start downloading the images
|
||||||
|
webpic.start_downloading('https://www.endmove.eu/', 'EndMove-website-images')
|
||||||
|
|
||||||
|
# We wait for the worker to start the task (once the task has started it cannot be stopped)
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
# Webpic will ask the program to stop in blocking mode (it will join the worker to wait for the end of its execution)
|
||||||
|
webpic.stop_downloading(True)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Improvement (TODO LIST)
|
||||||
|
|
||||||
|
Here you will find some improvements I would like to add to the program, you can also participate by forking the repository and submitting a pull request.
|
||||||
|
|
||||||
|
- [x] Check for updates button.
|
||||||
|
- [ ] Integrated file explorer.
|
||||||
|
- [ ] Viewing the downloads already made.
|
||||||
|
- [ ] Redo WebPicDownlodaer script to support concurrent downloads, to be able to launch workers and share tasks via a download pool.
|
||||||
|
|
||||||
|
This program is only a free utility tool and has not been developed in depth. In a future version it would be interesting to manage concurrent downloads in a thread pool.
|
7
app_metadata.yml
Normal file
7
app_metadata.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Version: 1.0.0
|
||||||
|
CompanyName: EndMove Corp.
|
||||||
|
FileDescription: Scraping tool to recover all the images of a website.
|
||||||
|
InternalName: webpic
|
||||||
|
LegalCopyright: © Copyright 2022 EndMove. All rights reserved.
|
||||||
|
OriginalFilename: WebPicDownloader.exe
|
||||||
|
ProductName: WebPicDownloader
|
101
build_config.json
Normal file
101
build_config.json
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"version": "auto-py-to-exe-configuration_v1",
|
||||||
|
"pyinstallerOptions": [
|
||||||
|
{
|
||||||
|
"optionDest": "noconfirm",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "filenames",
|
||||||
|
"value": "C:/Users/super/Documents/Developpement/Python/WebPicDownloader/main.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "onefile",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "console",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "icon_file",
|
||||||
|
"value": "C:/Users/super/Documents/Developpement/Python/WebPicDownloader/webpicdownloader/assets/logo.ico"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "name",
|
||||||
|
"value": "WebPicDownloader"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "ascii",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "clean_build",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "strip",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "noupx",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "disable_windowed_traceback",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "version_file",
|
||||||
|
"value": "C:/Users/super/Documents/Developpement/Python/WebPicDownloader/app_version_file.txt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "embed_manifest",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "uac_admin",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "uac_uiaccess",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "win_private_assemblies",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "win_no_prefer_redirects",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "bootloader_ignore_signals",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "argv_emulation",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "datas",
|
||||||
|
"value": "C:/Users/super/Documents/Developpement/Python/WebPicDownloader/webpicdownloader/assets;webpicdownloader/assets/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "excludes",
|
||||||
|
"value": "auto_py_to_exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "excludes",
|
||||||
|
"value": "pyinstaller"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"optionDest": "excludes",
|
||||||
|
"value": "unittest"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nonPyinstallerOptions": {
|
||||||
|
"increaseRecursionLimit": true,
|
||||||
|
"manualArguments": ""
|
||||||
|
}
|
||||||
|
}
|
16
build_tool.py
Normal file
16
build_tool.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import pyinstaller_versionfile
|
||||||
|
from auto_py_to_exe import __main__ as apte
|
||||||
|
from main import get_config
|
||||||
|
|
||||||
|
"""
|
||||||
|
This file allows you to start auto-py in order to quickly and easily build the solution
|
||||||
|
into a viable .exe. Moreover this script will compile the meta data for windows.
|
||||||
|
"""
|
||||||
|
if __name__ == '__main__':
|
||||||
|
pyinstaller_versionfile.create_versionfile_from_input_file(
|
||||||
|
output_file="app_version_file.txt",
|
||||||
|
input_file="app_metadata.yml",
|
||||||
|
version=get_config().get('app_version')
|
||||||
|
)
|
||||||
|
apte.__name__ = '__main__'
|
||||||
|
apte.run()
|
78
main.py
Normal file
78
main.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from webpicdownloader.model.WebPicDownloader import WebPicDownloader
|
||||||
|
from webpicdownloader.controller.HomeController import HomeController
|
||||||
|
from webpicdownloader.controller.InfoController import InfoController
|
||||||
|
from webpicdownloader.controller.MainController import MainController
|
||||||
|
from webpicdownloader.controller.Frames import Frames
|
||||||
|
from webpicdownloader.view.HomeView import HomeView
|
||||||
|
from webpicdownloader.view.InfoView import InfoView
|
||||||
|
from webpicdownloader.view.MainWindow import MainWindow
|
||||||
|
|
||||||
|
|
||||||
|
def get_sys_directory() -> str:
|
||||||
|
"""
|
||||||
|
Recover the path of the application's resources.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
directory = sys._MEIPASS
|
||||||
|
except Exception:
|
||||||
|
directory = os.getcwd()
|
||||||
|
return directory
|
||||||
|
|
||||||
|
def get_config() -> dict:
|
||||||
|
"""
|
||||||
|
Retrieve the application configuration
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'app_name': 'WebPicDownloader',
|
||||||
|
'app_folder': os.getcwd(),
|
||||||
|
'app_version': '1.0.0', # This version must match with the version.txt at root
|
||||||
|
'app_version_date': '2022-09-11',
|
||||||
|
'app_depo_version': 'https://git.endmove.eu/EndMove/WebPicDownloader/raw/branch/master/VERSION',
|
||||||
|
'app_depo_releases': 'https://git.endmove.eu/EndMove/WebPicDownloader/releases',
|
||||||
|
|
||||||
|
'sys_version_matcher': re.compile(r'^(\d{1,2}\.)(\d{1,2}\.)(\d{1,2})$'),
|
||||||
|
'sys_directory': get_sys_directory(),
|
||||||
|
|
||||||
|
'about_title': 'About WebPicDownloader',
|
||||||
|
'about_content': "This scraping software has been developed by EndMove\nand is fully open-source. The source code is available\nhere: https://git.endmove.eu/EndMove/WebPicDownloader\nEndMove is available at the following address for any\nrequest contact@endmove.eu. In case of problemsplease\nopen an issue on the repository.\n\nThe logo of the software was made by Gashila"
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
"""
|
||||||
|
WebPicDownloader is a program developed and maintened by EndMove under Apache 2.0 License. Stealing code is a crime !
|
||||||
|
Disclamer : The developer of this application can in no way be held responsible if his application is used for illegal purposes.
|
||||||
|
|
||||||
|
@author Jérémi Nihart / EndMove
|
||||||
|
@link https://git.endmove.eu/EndMove/WebPicDownloader
|
||||||
|
@version 1.0.0
|
||||||
|
@since 2022-08-30
|
||||||
|
"""
|
||||||
|
# configuration
|
||||||
|
config = get_config()
|
||||||
|
|
||||||
|
# Create utli/model
|
||||||
|
webpic = WebPicDownloader(path=config.get('app_folder'))
|
||||||
|
|
||||||
|
# Create app controllers
|
||||||
|
main_controller = MainController(config)
|
||||||
|
home_controller = HomeController(main_controller, webpic)
|
||||||
|
info_controller = InfoController(main_controller)
|
||||||
|
|
||||||
|
# Create app views
|
||||||
|
main_window = MainWindow(main_controller)
|
||||||
|
home_view = HomeView(main_window, home_controller)
|
||||||
|
info_controller = InfoView(main_window, info_controller)
|
||||||
|
|
||||||
|
# Add views to main window
|
||||||
|
main_window.add_view(Frames.HOME, home_view)
|
||||||
|
main_window.add_view(Frames.INFO, info_controller)
|
||||||
|
|
||||||
|
# Choose the launching view
|
||||||
|
main_window.show_frame(Frames.HOME)
|
||||||
|
|
||||||
|
# Start main windows looping (launch program)
|
||||||
|
|
||||||
|
main_window.mainloop()
|
11
run_tests.py
Normal file
11
run_tests.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
This file allows you to launch all the unit tests of the project.
|
||||||
|
|
||||||
|
Be careful these tests require internet.
|
||||||
|
"""
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
runner = unittest.TextTestRunner()
|
||||||
|
runner.run(loader.discover('./tests/'))
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
48
tests/test_update_regex.py
Normal file
48
tests/test_update_regex.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import unittest
|
||||||
|
from re import fullmatch, compile, Pattern
|
||||||
|
|
||||||
|
|
||||||
|
class TestUpdateRegex(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Test Class for Update Regex
|
||||||
|
|
||||||
|
Versions can vary between 0.0.0 and 99.99.99 and cannot exceed two units.
|
||||||
|
0.0.0 to 0.99.99 = BETA
|
||||||
|
1.0.0 to 99.99.99 = RELEASE
|
||||||
|
"""
|
||||||
|
regex: Pattern = compile(r'^(\d{1,2}\.)(\d{1,2}\.)(\d{1,2})$')
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_version(self):
|
||||||
|
self.assertTrue(fullmatch(self.regex, '1.0.0'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_patch_version(self):
|
||||||
|
self.assertTrue(fullmatch(self.regex, '1.0.00'))
|
||||||
|
self.assertTrue(fullmatch(self.regex, '1.0.1'))
|
||||||
|
self.assertTrue(fullmatch(self.regex, '1.0.12'))
|
||||||
|
self.assertTrue(fullmatch(self.regex, '1.0.54'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_minor_version(self):
|
||||||
|
self.assertTrue(fullmatch(self.regex, '1.00.0'))
|
||||||
|
self.assertTrue(fullmatch(self.regex, '1.1.0'))
|
||||||
|
self.assertTrue(fullmatch(self.regex, '1.98.0'))
|
||||||
|
self.assertTrue(fullmatch(self.regex, '1.24.0'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_major_version(self):
|
||||||
|
self.assertTrue(fullmatch(self.regex, '00.0.0'))
|
||||||
|
self.assertTrue(fullmatch(self.regex, '10.0.0'))
|
||||||
|
self.assertTrue(fullmatch(self.regex, '99.0.0'))
|
||||||
|
self.assertTrue(fullmatch(self.regex, '42.0.0'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_version(self):
|
||||||
|
self.assertFalse(fullmatch(self.regex, '1.0.0000'))
|
||||||
|
self.assertFalse(fullmatch(self.regex, '1.00000.0'))
|
||||||
|
self.assertFalse(fullmatch(self.regex, '100.0.0'))
|
||||||
|
self.assertFalse(fullmatch(self.regex, '152.124.15'))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
74
tests/test_update_utils.py
Normal file
74
tests/test_update_utils.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import unittest
|
||||||
|
from urllib.error import HTTPError, URLError
|
||||||
|
from test_update_regex import TestUpdateRegex
|
||||||
|
from webpicdownloader.util.UpdateUtils import fetch_version, check_for_update
|
||||||
|
|
||||||
|
class TestUpdateUtils(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Test Class for Update Utils
|
||||||
|
<!> can not be executed directly <!>
|
||||||
|
|
||||||
|
* regex -> use UpdateRegex unittest regex.
|
||||||
|
* good_url -> use a permalink VERSION file.
|
||||||
|
* bad_url -> bad link that point a 404 error.
|
||||||
|
"""
|
||||||
|
regex = TestUpdateRegex.regex
|
||||||
|
good_url = 'https://git.endmove.eu/EndMove/WebPicDownloader/raw/commit/6b26b89c6901841faaa09154c185d202223492c2/VERSION'
|
||||||
|
bad_url = 'https://git.endmove.eu/EndMove/WebPicDownloader/raw/commit/bad-commit/VERSION'
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_version__good_url(self):
|
||||||
|
"""
|
||||||
|
We test the recovery of the available version with a URL
|
||||||
|
indicating a correct version file.
|
||||||
|
"""
|
||||||
|
self.assertTrue(fetch_version(self.good_url) == '1.0.0')
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_version__bad_url(self):
|
||||||
|
"""
|
||||||
|
We test the recovery of the version with erroneous URLs,
|
||||||
|
or not leading to any specific file
|
||||||
|
"""
|
||||||
|
self.assertRaises(HTTPError, lambda: fetch_version(self.bad_url))
|
||||||
|
self.assertRaises(URLError, lambda: fetch_version('https://bad'))
|
||||||
|
self.assertRaises(ValueError, lambda: fetch_version('bad'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_for_update__new_version(self):
|
||||||
|
"""
|
||||||
|
We check the availability of an update with softwares whose
|
||||||
|
versions are different from the one indicated by the repository
|
||||||
|
(thus available update).
|
||||||
|
"""
|
||||||
|
self.assertTrue(check_for_update('0.0.0', self.good_url, self.regex))
|
||||||
|
self.assertTrue(check_for_update('0.48.0', self.good_url, self.regex))
|
||||||
|
self.assertTrue(check_for_update('0.48.14', self.good_url, self.regex))
|
||||||
|
self.assertTrue(check_for_update('5.48.14', self.good_url, self.regex))
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_for_update__no_new_version(self):
|
||||||
|
"""
|
||||||
|
We check the availability of an update with software that has the
|
||||||
|
same version as the one on the repository (so no update available).
|
||||||
|
"""
|
||||||
|
self.assertFalse(check_for_update('1.0.0', self.good_url, self.regex))
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_for_update__bad_version(self):
|
||||||
|
"""
|
||||||
|
We check for the availability of an update with an incorrect version
|
||||||
|
in the repository.
|
||||||
|
"""
|
||||||
|
self.assertRaises(ValueError, lambda: check_for_update(
|
||||||
|
'1.0.0',
|
||||||
|
'https://git.endmove.eu/EndMove/WebPicDownloader/src/commit/6b26b89c6901841faaa09154c185d202223492c2/app_version_file.txt',
|
||||||
|
self.regex
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_for_update__bad_url(self):
|
||||||
|
"""
|
||||||
|
We check the availability of an update with a bad repository url
|
||||||
|
"""
|
||||||
|
self.assertRaises(HTTPError, lambda: check_for_update('1.0.0', self.bad_url, self.regex))
|
BIN
webpicdownloader/assets/logo.ico
Normal file
BIN
webpicdownloader/assets/logo.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 264 KiB |
18
webpicdownloader/controller/Frames.py
Normal file
18
webpicdownloader/controller/Frames.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class Frames(Enum):
|
||||||
|
"""
|
||||||
|
Enumeration - Frames
|
||||||
|
|
||||||
|
Lists the different windows of the program in order to facilitate
|
||||||
|
their call during the execution. Each parameter of the enumeration
|
||||||
|
represents a Frame/Tab.
|
||||||
|
|
||||||
|
@author Jérémi Nihart / EndMove
|
||||||
|
@link https://git.endmove.eu/EndMove/WebPicDownloader
|
||||||
|
@version 1.0.0
|
||||||
|
@since 2022-08-30
|
||||||
|
"""
|
||||||
|
HOME = 1 # Home view
|
||||||
|
INFO = 2 # Info & copyright view
|
118
webpicdownloader/controller/HomeController.py
Normal file
118
webpicdownloader/controller/HomeController.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
from webpicdownloader.controller.MainController import MainController
|
||||||
|
from webpicdownloader.model.WebPicDownloader import MessageType, WebPicDownloader
|
||||||
|
|
||||||
|
|
||||||
|
class HomeController:
|
||||||
|
"""
|
||||||
|
Controller - HomeController
|
||||||
|
|
||||||
|
This controller handles all the interaction directly related to the download.
|
||||||
|
|
||||||
|
@author Jérémi Nihart / EndMove
|
||||||
|
@link https://git.endmove.eu/EndMove/WebPicDownloader
|
||||||
|
@version 1.0.0
|
||||||
|
@since 2022-08-30
|
||||||
|
"""
|
||||||
|
# Variables
|
||||||
|
__main_controller: MainController = None
|
||||||
|
__view = None
|
||||||
|
__webpic: WebPicDownloader = None
|
||||||
|
|
||||||
|
# Constructor
|
||||||
|
def __init__(self, controller: MainController, webpic: WebPicDownloader) -> None:
|
||||||
|
"""
|
||||||
|
Constructor
|
||||||
|
|
||||||
|
* :controller: -> The main application cpntroller.
|
||||||
|
* :webpic: -> The webpicdownloader instance.
|
||||||
|
"""
|
||||||
|
# Setub variables
|
||||||
|
self.__main_controller = controller
|
||||||
|
self.__webpic = webpic
|
||||||
|
|
||||||
|
# setup webpic event
|
||||||
|
webpic.set_messenger_callback(self.on_webpic_messenger)
|
||||||
|
webpic.set_success_callback(self.on_webpic_success)
|
||||||
|
webpic.set_failure_callback(self.on_webpic_failure)
|
||||||
|
|
||||||
|
# Subscribe to events
|
||||||
|
controller.subscribe_to_quite_event(self.on_quit)
|
||||||
|
|
||||||
|
# START View methods
|
||||||
|
def set_view(self, view) -> None:
|
||||||
|
"""
|
||||||
|
[function for view]
|
||||||
|
=> Define the view of this controller.
|
||||||
|
|
||||||
|
* :view: -> The view that this controller manage.
|
||||||
|
"""
|
||||||
|
self.__view = view
|
||||||
|
# END View method
|
||||||
|
|
||||||
|
# START View events
|
||||||
|
def on_download_requested(self, url: str, name: str) -> None:
|
||||||
|
"""
|
||||||
|
[event function for view]
|
||||||
|
=> Call this event method when the user requests to download
|
||||||
|
|
||||||
|
* :url: -> The url of the website to use for pic-download.
|
||||||
|
* :name: -> The name of the folder in which put pictures.
|
||||||
|
"""
|
||||||
|
if url.strip() and name.strip():
|
||||||
|
self.__view.set_interface_state(True)
|
||||||
|
self.__view.clear_logs()
|
||||||
|
self.__webpic.start_downloading(url, name)
|
||||||
|
else:
|
||||||
|
self.__view.show_error_message("Opss, the url or folder name are not valid!")
|
||||||
|
# END View events
|
||||||
|
|
||||||
|
# START Webpic events
|
||||||
|
def on_webpic_messenger(self, message: str, type) -> None:
|
||||||
|
"""
|
||||||
|
[event function for webpic]
|
||||||
|
=> This event is called to communicate a message.
|
||||||
|
|
||||||
|
* :message: -> Message that webpic send to the controller.
|
||||||
|
* :type: -> Type of message that webpic send to the controller.
|
||||||
|
"""
|
||||||
|
match type:
|
||||||
|
case MessageType.LOG:
|
||||||
|
self.__view.add_log(message)
|
||||||
|
case MessageType.ERROR:
|
||||||
|
self.__view.show_error_message(message)
|
||||||
|
case MessageType.SUCCESS:
|
||||||
|
self.__view.show_success_message(message)
|
||||||
|
|
||||||
|
def on_webpic_success(self) -> None:
|
||||||
|
"""
|
||||||
|
[event function for webpic]
|
||||||
|
=> This event is called to indicate that the download has finished successfully.
|
||||||
|
"""
|
||||||
|
self.__view.show_success_message("The download has been successfully completed.")
|
||||||
|
self.__view.set_interface_state(False)
|
||||||
|
|
||||||
|
def on_webpic_failure(self) -> None:
|
||||||
|
"""
|
||||||
|
[event function for webpic]
|
||||||
|
=> This event is called to indicate that there was a problem during the download.
|
||||||
|
"""
|
||||||
|
self.__view.show_error_message("A critical error preventing the download occurred, check the logs.")
|
||||||
|
self.__view.set_interface_state(False)
|
||||||
|
# END Webpic events
|
||||||
|
|
||||||
|
# START Controller methods
|
||||||
|
def on_quit(self) -> bool:
|
||||||
|
"""
|
||||||
|
[event function for controller]
|
||||||
|
=> Call this event when a request to exit is thrown.
|
||||||
|
"""
|
||||||
|
if self.__webpic.is_download_running():
|
||||||
|
if self.__main_controller.show_question_dialog(
|
||||||
|
"Are you sure?",
|
||||||
|
"Do you really want to quit while the download is running?\nThis will stop the download."
|
||||||
|
):
|
||||||
|
self.__webpic.stop_downloading() # hot stop deamon
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
self.__webpic.stop_downloading(block=True)
|
||||||
|
# END Controller methods
|
54
webpicdownloader/controller/InfoController.py
Normal file
54
webpicdownloader/controller/InfoController.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from webpicdownloader.controller.Frames import Frames
|
||||||
|
from webpicdownloader.controller.MainController import MainController
|
||||||
|
|
||||||
|
|
||||||
|
class InfoController:
|
||||||
|
"""
|
||||||
|
Controller - InfoController
|
||||||
|
|
||||||
|
This controller manages the display of information in the information view.
|
||||||
|
|
||||||
|
@author Jérémi Nihart / EndMove
|
||||||
|
@link https://git.endmove.eu/EndMove/WebPicDownloader
|
||||||
|
@version 1.0.0
|
||||||
|
@since 2022-08-30
|
||||||
|
"""
|
||||||
|
# Variables
|
||||||
|
__main_controller: MainController = None
|
||||||
|
__view = None
|
||||||
|
|
||||||
|
# Constructor
|
||||||
|
def __init__(self, controller: MainController) -> None:
|
||||||
|
"""
|
||||||
|
Constructor
|
||||||
|
|
||||||
|
* :controller: -> The main application cpntroller.
|
||||||
|
"""
|
||||||
|
# Setup variables
|
||||||
|
self.__main_controller = controller
|
||||||
|
|
||||||
|
# START View methods
|
||||||
|
def set_view(self, view) -> None:
|
||||||
|
"""
|
||||||
|
[function for view]
|
||||||
|
|
||||||
|
:view: -> The view that this controller manage.
|
||||||
|
"""
|
||||||
|
self.__view = view
|
||||||
|
self.__view.set_title(self.__main_controller.get_config('about_title'))
|
||||||
|
self.__view.set_content(self.__main_controller.get_config('about_content'))
|
||||||
|
self.__view.set_version(
|
||||||
|
f"version: {self.__main_controller.get_config('app_version')} - {self.__main_controller.get_config('app_version_date')}"
|
||||||
|
)
|
||||||
|
# END View method
|
||||||
|
|
||||||
|
# START View events
|
||||||
|
def on_change_view(self, frame: Frames) -> None:
|
||||||
|
"""
|
||||||
|
[event function for view]
|
||||||
|
=> Call this event method when the user requests to change the window.
|
||||||
|
|
||||||
|
* :frame: -> The frame we want to launch.
|
||||||
|
"""
|
||||||
|
self.__main_controller.change_frame(frame)
|
||||||
|
# END View events
|
142
webpicdownloader/controller/MainController.py
Normal file
142
webpicdownloader/controller/MainController.py
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import os
|
||||||
|
import webbrowser
|
||||||
|
from webpicdownloader.controller.Frames import Frames
|
||||||
|
from webpicdownloader.util.UpdateUtils import check_for_update
|
||||||
|
|
||||||
|
|
||||||
|
class MainController:
|
||||||
|
"""
|
||||||
|
Controller - MainController
|
||||||
|
|
||||||
|
This controller manages all the main interaction, change of windows,
|
||||||
|
dialogs, stop... It is the main controller.
|
||||||
|
|
||||||
|
@author Jérémi Nihart / EndMove
|
||||||
|
@link https://git.endmove.eu/EndMove/WebPicDownloader
|
||||||
|
@version 1.0.1
|
||||||
|
@since 2022-09-11
|
||||||
|
"""
|
||||||
|
# Variables
|
||||||
|
__config: dict = None
|
||||||
|
__view = None
|
||||||
|
__quite_event_subscribers: list = None
|
||||||
|
|
||||||
|
# Constructor
|
||||||
|
def __init__(self, config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Constructor
|
||||||
|
|
||||||
|
* :config: -> The application configuration (a dictionary).
|
||||||
|
"""
|
||||||
|
# Setup variables
|
||||||
|
self.__config = config
|
||||||
|
self.__quite_event_subscribers = []
|
||||||
|
|
||||||
|
# START View methods
|
||||||
|
def set_view(self, view) -> None:
|
||||||
|
"""
|
||||||
|
[function for view]
|
||||||
|
=> Allow to define the controller view.
|
||||||
|
|
||||||
|
* :view: -> The view that this controller manage and setup it.
|
||||||
|
"""
|
||||||
|
self.__view = view
|
||||||
|
view.set_window_title(self.get_config('app_name'))
|
||||||
|
|
||||||
|
def on_open_folder(self) -> None:
|
||||||
|
"""
|
||||||
|
[event function for view]
|
||||||
|
=> Event launch when you ask to open the current folder.
|
||||||
|
"""
|
||||||
|
os.startfile(self.get_config('app_folder')) # Open the file explorer on working dir
|
||||||
|
|
||||||
|
def on_quite(self) -> None:
|
||||||
|
"""
|
||||||
|
[event function for view]
|
||||||
|
=> Event launch when you ask to quit the program. This event is propagated
|
||||||
|
to the subscribers, they can eventually cancel the event
|
||||||
|
"""
|
||||||
|
for callback in self.__quite_event_subscribers:
|
||||||
|
if callback():
|
||||||
|
return
|
||||||
|
self.__view.close_window() # End the program
|
||||||
|
|
||||||
|
def on_check_for_update(self) -> None:
|
||||||
|
"""
|
||||||
|
[event function for view]
|
||||||
|
=> Event launched when a check for available updates is requested.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if check_for_update(self.get_config('app_version'), self.get_config('app_depo_version'), self.get_config('sys_version_matcher')):
|
||||||
|
if self.show_question_dialog(self.get_config('app_name'), "An update is available! Would you like to visit the release page to download the latest version?"):
|
||||||
|
webbrowser.open(self.get_config('app_depo_releases'))
|
||||||
|
else: self.show_information_dialog(self.get_config('app_name'), "No update available.")
|
||||||
|
except Exception as e:
|
||||||
|
self.show_information_dialog(self.get_config('app_name'), f"An error occured: {e}.", 'error')
|
||||||
|
|
||||||
|
def on_about(self) -> None:
|
||||||
|
"""
|
||||||
|
[event function for view]
|
||||||
|
=> Event launched when a request for more information arise.
|
||||||
|
"""
|
||||||
|
self.change_frame(Frames.INFO)
|
||||||
|
# END View methods
|
||||||
|
|
||||||
|
# START Controller methods
|
||||||
|
def change_frame(self, frame: Frames) -> None:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Allows you to request a frame change in the main window.
|
||||||
|
|
||||||
|
* :frame: -> The frame we want to display on the window instead of the current frame.
|
||||||
|
"""
|
||||||
|
self.__view.show_frame(frame)
|
||||||
|
|
||||||
|
def get_config(self, name: str):
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Allows controllers to access the application's configuration.
|
||||||
|
|
||||||
|
* :name: -> The name of the configuration parameter for which we want to access the configured value.
|
||||||
|
* THROWABLE -> If the key is wrong, throw a ValueError.
|
||||||
|
"""
|
||||||
|
if self.__config.get(name):
|
||||||
|
return self.__config.get(name)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unable to find a configuration with this name")
|
||||||
|
|
||||||
|
def show_question_dialog(self, title: str='title', message: str='question?', icon: str='question') -> bool:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Ask a question to the user and block until he answers with yes or no.
|
||||||
|
|
||||||
|
* :title: -> Title of the dialogue.
|
||||||
|
* :message: -> Message of the dialogue.
|
||||||
|
* :icon: -> Icon of the dialogue
|
||||||
|
"""
|
||||||
|
return self.__view.show_question_dialog(title, message, icon)
|
||||||
|
|
||||||
|
def show_information_dialog(self, title: str='title', message: str='informations!', icon: str='info') -> bool:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Display a pop-up information dialog to the user.
|
||||||
|
|
||||||
|
* :title: -> Title of the dialogue.
|
||||||
|
* :message: -> Message of the dialogue.
|
||||||
|
* :icon: -> Icon of the dialogue
|
||||||
|
"""
|
||||||
|
return self.__view.show_information_dialog(title, message, icon)
|
||||||
|
# END Controller methods
|
||||||
|
|
||||||
|
# START Controller events
|
||||||
|
def subscribe_to_quite_event(self, callback) -> None:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Subscription function allowing to be warned if a request to quit occurs.
|
||||||
|
In the case where the callback function returns False the process continues
|
||||||
|
but if the callback returns True the process is aborted.
|
||||||
|
|
||||||
|
* :callback: -> Callback function that will be called when a request to exit occurs.
|
||||||
|
"""
|
||||||
|
self.__quite_event_subscribers.append(callback)
|
||||||
|
# END Controller events
|
333
webpicdownloader/model/WebPicDownloader.py
Normal file
333
webpicdownloader/model/WebPicDownloader.py
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
import os
|
||||||
|
from enum import Enum
|
||||||
|
from threading import Semaphore, Thread
|
||||||
|
from urllib import request
|
||||||
|
from bs4 import BeautifulSoup, Tag, ResultSet
|
||||||
|
|
||||||
|
|
||||||
|
class MessageType(Enum):
|
||||||
|
"""
|
||||||
|
MessageType
|
||||||
|
|
||||||
|
Is an enumeration to define the different types of messages sent by the webpic messenger.
|
||||||
|
|
||||||
|
There are 3 types of messages.
|
||||||
|
- log -> log
|
||||||
|
- error -> err
|
||||||
|
- success -> suc
|
||||||
|
|
||||||
|
@author Jérémi Nihart / EndMove
|
||||||
|
@link https://git.endmove.eu/EndMove/WebPicDownloader
|
||||||
|
@version 1.0.0
|
||||||
|
@since 2022-09-05
|
||||||
|
"""
|
||||||
|
LOG = 'log'
|
||||||
|
ERROR = 'err'
|
||||||
|
SUCCESS = 'suc'
|
||||||
|
|
||||||
|
|
||||||
|
class WebPicDownloader(Thread):
|
||||||
|
"""
|
||||||
|
WebPicDownloader
|
||||||
|
|
||||||
|
Webpicdownloader is a scraping tool that allows you to browse a web page,
|
||||||
|
find the images and download them. This tool is easily usable and implementable
|
||||||
|
in an application. It has been designed to be executed in an integrated thread
|
||||||
|
in an asynchronous way. This tool allows to define 3 callback functions, one for
|
||||||
|
events, one in case of success and one in case of failure. It also has an
|
||||||
|
integrated entry point allowing it to be directly executed in terminal mode.
|
||||||
|
|
||||||
|
@author EndMove <contact@endmove.eu>
|
||||||
|
@link https://git.endmove.eu/EndMove/WebPicDownloader
|
||||||
|
@version 1.2.1
|
||||||
|
@since 2022-09-05
|
||||||
|
"""
|
||||||
|
# Variables
|
||||||
|
__callbacks: dict = None # Callback dictionary
|
||||||
|
__settings: dict = None # Webpic basics settings
|
||||||
|
__dl_infos: dict = None # Download informations
|
||||||
|
__sem: Semaphore = None # Semaphore for the webpic worker
|
||||||
|
|
||||||
|
_exit: bool = None # When set to True quit the thread
|
||||||
|
|
||||||
|
# Constructor
|
||||||
|
def __init__(self, path: str = None, headers: dict = None, messenger = None,
|
||||||
|
success = None, failure = None) -> None:
|
||||||
|
"""
|
||||||
|
Constructor
|
||||||
|
=> It is important to initialize the WebPicDownloader object properly. The callback
|
||||||
|
functions can be initialized after the creation of the object.
|
||||||
|
|
||||||
|
* :path: -> Folder in which the tool will create the download folders and place the images.
|
||||||
|
* :headers: -> Dictionary allowing to define the different parameters present in the header
|
||||||
|
of the requests sent by WebPic.
|
||||||
|
* :messenger: -> Callback function messenger (see setter).
|
||||||
|
* :success: -> Callback function success (see setter).
|
||||||
|
* :failure: -> Callback function failure (see setter).
|
||||||
|
"""
|
||||||
|
super().__init__(daemon=True, name='WebPic download worker')
|
||||||
|
self.__callbacks = {
|
||||||
|
'messenger': messenger if messenger else lambda msg, type: print(msg),
|
||||||
|
'success': success if success else lambda: print("Success!"),
|
||||||
|
'failure': failure if failure else lambda: print("failure!")
|
||||||
|
}
|
||||||
|
self.__settings = {
|
||||||
|
'root_path': path if path else os.getcwd(),
|
||||||
|
'headers': headers if headers else {
|
||||||
|
'User-Agent': "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.__dl_infos = {
|
||||||
|
'website_url': 'url',
|
||||||
|
'download_name': 'name',
|
||||||
|
'download_path': 'full_path',
|
||||||
|
'running': False
|
||||||
|
}
|
||||||
|
self.__sem = Semaphore(0)
|
||||||
|
self.__exit = False
|
||||||
|
|
||||||
|
self.start() # start deamon
|
||||||
|
|
||||||
|
|
||||||
|
# Internal functions
|
||||||
|
def __get_html(self, url: str) -> str:
|
||||||
|
"""
|
||||||
|
Internal Function #do-not-use#
|
||||||
|
=> Allow to retrieve the HTML content of a website.
|
||||||
|
|
||||||
|
* :url: -> The url of the site for which we want to get the content of the HTML page.
|
||||||
|
* RETURN -> Web page content.
|
||||||
|
"""
|
||||||
|
req = request.Request(url, headers=self.__settings.get('headers'))
|
||||||
|
response = request.urlopen(req)
|
||||||
|
return response.read().decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def __find_all_img(self, html: str) -> ResultSet:
|
||||||
|
"""
|
||||||
|
Internal Function #do-not-use#
|
||||||
|
=> Allow to retrieve all images of an html page.
|
||||||
|
|
||||||
|
* :html: -> Html code in which to search for image balises.
|
||||||
|
* RETURN -> Iterable with all image balises.
|
||||||
|
"""
|
||||||
|
soup = BeautifulSoup(html, 'html.parser')
|
||||||
|
return soup.find_all('img')
|
||||||
|
|
||||||
|
|
||||||
|
def __find_img_link(self, img: Tag) -> str:
|
||||||
|
"""
|
||||||
|
Internal Function #do-not-use#
|
||||||
|
=> Allow to retrieve the link of a picture.
|
||||||
|
|
||||||
|
* :img: -> Image tag {@code bs4.Tag} for which to search the link of an image.
|
||||||
|
* RETURN -> Image link.
|
||||||
|
"""
|
||||||
|
if img.get('src'):
|
||||||
|
link = img.get('src')
|
||||||
|
elif img.get('data-src'):
|
||||||
|
link = img.get('data-src')
|
||||||
|
elif img.get('data-srcset'):
|
||||||
|
link = img.get('data-srcset')
|
||||||
|
elif img.get('data-fallback-src'):
|
||||||
|
link = img.get('data-fallback-src')
|
||||||
|
else:
|
||||||
|
raise ValueError("Unable to find image url")
|
||||||
|
if not 'http' in link:
|
||||||
|
raise ValueError("Bad image url")
|
||||||
|
return link
|
||||||
|
|
||||||
|
|
||||||
|
def __find_image_type(self, img_link: str) -> str:
|
||||||
|
"""
|
||||||
|
Internal Function #do-not-use#
|
||||||
|
=> Allow to retrieve the right image type (png, jpeg...)
|
||||||
|
|
||||||
|
* :img_link: -> Lien de l'image pourllaquel trouver le bon type.
|
||||||
|
* RETURN -> Type of image.
|
||||||
|
"""
|
||||||
|
type = img_link.split('.')[-1]
|
||||||
|
if '?' in type:
|
||||||
|
type = type.split('?')[0]
|
||||||
|
return type
|
||||||
|
|
||||||
|
|
||||||
|
def __download_img(self, url: str, filename: str) -> None:
|
||||||
|
"""
|
||||||
|
Internal Function #do-not-use#
|
||||||
|
=> Allow to download a picture from internet
|
||||||
|
|
||||||
|
* :url: -> Image url on the web.
|
||||||
|
* :filename: -> Full path with name of the future image.
|
||||||
|
"""
|
||||||
|
req = request.Request(url, headers=self.__settings.get('headers'))
|
||||||
|
raw_img = request.urlopen(req).read()
|
||||||
|
with open(filename, 'wb') as img:
|
||||||
|
img.write(raw_img)
|
||||||
|
|
||||||
|
|
||||||
|
def __initialize_folder(self, folder_path: str) -> None:
|
||||||
|
"""
|
||||||
|
Internal Function #do-not-use#
|
||||||
|
=> Checks if the folder in which to place the images to be uploaded exists and if
|
||||||
|
not chalk it up. An exception is raised if this folder already exists.
|
||||||
|
|
||||||
|
* :folder_path: -> Full path to the working folder (for the download task).
|
||||||
|
"""
|
||||||
|
if not os.path.exists(folder_path):
|
||||||
|
os.mkdir(folder_path)
|
||||||
|
else:
|
||||||
|
raise ValueError("The folder already exists, it may already contain images")
|
||||||
|
|
||||||
|
|
||||||
|
def __msg(self, message: str, type:MessageType=MessageType.LOG) -> None:
|
||||||
|
"""
|
||||||
|
Internal Function #do-not-use#
|
||||||
|
=> Use the messenger callback to send a message.
|
||||||
|
|
||||||
|
* :message: -> the message to send through callback
|
||||||
|
* :type: -> message type, can be ['log', 'err', 'suc']
|
||||||
|
"""
|
||||||
|
self.__callbacks.get('messenger')(message, type)
|
||||||
|
|
||||||
|
|
||||||
|
# Public functions
|
||||||
|
def set_success_callback(self, callback) -> None:
|
||||||
|
"""
|
||||||
|
Setter to define the callback function when the download succeeded.
|
||||||
|
|
||||||
|
* :callback: -> the callback function to call when the download is a success.
|
||||||
|
"""
|
||||||
|
self.__callbacks['success'] = callback
|
||||||
|
|
||||||
|
|
||||||
|
def set_failure_callback(self, callback) -> None:
|
||||||
|
"""
|
||||||
|
Setter to define the callback function called when the download fails.
|
||||||
|
|
||||||
|
* :callback: -> the callback function to call when the download is a failure.
|
||||||
|
"""
|
||||||
|
self.__callbacks['failure'] = callback
|
||||||
|
|
||||||
|
|
||||||
|
def set_messenger_callback(self, callback) -> None:
|
||||||
|
"""
|
||||||
|
Setter to define the callback function called when new messages arrive.
|
||||||
|
|
||||||
|
* :callback: -> the callback function to call when a message event is emited.
|
||||||
|
"""
|
||||||
|
self.__callbacks['messenger'] = callback
|
||||||
|
|
||||||
|
|
||||||
|
def start_downloading(self, url: str, name: str) -> None:
|
||||||
|
"""
|
||||||
|
Start downloading all pictures of a website.
|
||||||
|
|
||||||
|
* :url: -> The url of the website to annalyse.
|
||||||
|
* :folder_name: -> The name of the folder in which to upload the photos.
|
||||||
|
"""
|
||||||
|
if not self.is_alive:
|
||||||
|
self.__msg("Opss, the download thread is not running, please restart webpic.", MessageType.ERROR)
|
||||||
|
elif self.__dl_infos.get('running'):
|
||||||
|
self.__msg("Opss, the download thread is busy.", MessageType.ERROR)
|
||||||
|
else:
|
||||||
|
self.__dl_infos['website_url'] = url
|
||||||
|
self.__dl_infos['download_name'] = name
|
||||||
|
self.__sem.release()
|
||||||
|
|
||||||
|
|
||||||
|
def stop_downloading(self, block=False) -> None:
|
||||||
|
"""
|
||||||
|
Stops the download after the current item is processed and exit the downloading thread.
|
||||||
|
|
||||||
|
<!> Attention once called it will not be possible any more to download. <!>
|
||||||
|
|
||||||
|
* :block: -> If true, the function will block until the worker has finished working, if
|
||||||
|
False(default value), the stop message will be thrown and the program will continue.
|
||||||
|
"""
|
||||||
|
self.__exit = True
|
||||||
|
self.__sem.release()
|
||||||
|
if block:
|
||||||
|
self.join()
|
||||||
|
|
||||||
|
|
||||||
|
def is_download_running(self) -> bool:
|
||||||
|
"""
|
||||||
|
Indique si un téléchargement est en cours
|
||||||
|
|
||||||
|
* RETURN -> True if yes, False else.
|
||||||
|
"""
|
||||||
|
return self.__dl_infos['running'];
|
||||||
|
|
||||||
|
|
||||||
|
# Thread corp function
|
||||||
|
def run(self) -> None:
|
||||||
|
while True:
|
||||||
|
self.__sem.acquire() # waiting the authorization to process
|
||||||
|
|
||||||
|
if self.__exit: # check if the exiting is requested
|
||||||
|
return
|
||||||
|
|
||||||
|
self.__dl_infos['running'] = True # indicate that the thread is busy
|
||||||
|
|
||||||
|
try:
|
||||||
|
# parse infos from url
|
||||||
|
html = self.__get_html(self.__dl_infos.get('website_url')) # website html
|
||||||
|
images = self.__find_all_img(html) # find all img balises ing html
|
||||||
|
|
||||||
|
# setting up download informaations
|
||||||
|
tot_count = len(images) # count total image
|
||||||
|
dl_count = 0 # set download count to 0
|
||||||
|
self.__dl_infos['download_path'] = f"{self.__settings.get('root_path')}/{self.__dl_infos.get('download_name')}/" # format path
|
||||||
|
|
||||||
|
# init working directory
|
||||||
|
self.__initialize_folder(self.__dl_infos.get('download_path')) # Init download folder
|
||||||
|
self.__msg(f"WebPicDownloader found {tot_count} images on the website.")
|
||||||
|
|
||||||
|
# start images processing
|
||||||
|
for i, img in enumerate(images):
|
||||||
|
try:
|
||||||
|
self.__msg(f"Start downloading image {i}.")
|
||||||
|
|
||||||
|
img_link = self.__find_img_link(img) # find image link
|
||||||
|
self.__download_img(img_link, f"{self.__dl_infos.get('download_path')}image-{i}.{self.__find_image_type(img_link)}") # download the image
|
||||||
|
|
||||||
|
self.__msg(f"Download of image {i}, done!")
|
||||||
|
dl_count += 1 # increment download counter
|
||||||
|
except Exception as err:
|
||||||
|
self.__msg(f"ERROR: Unable to process image {i} -> err[{err}].")
|
||||||
|
# end images processing
|
||||||
|
|
||||||
|
self.__msg(f"WebPicDownloader has processed {dl_count} images out of {tot_count}.")
|
||||||
|
self.__callbacks.get('success')() # success, launch callback
|
||||||
|
except Exception as err:
|
||||||
|
self.__msg(f"ERROR: An error occured -> err[{err}]")
|
||||||
|
self.__callbacks.get('failure')() # error, launch callback
|
||||||
|
|
||||||
|
self.__dl_infos['running'] = False # inficate that the thread is free
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Internal entry point for testing and console use.
|
||||||
|
import time
|
||||||
|
|
||||||
|
wpd = WebPicDownloader() # Instance of webpic
|
||||||
|
|
||||||
|
# Callback functions
|
||||||
|
def success():
|
||||||
|
print("\nDownload completed with success.")
|
||||||
|
|
||||||
|
def failed():
|
||||||
|
print("\nDownload completed with errors.")
|
||||||
|
|
||||||
|
# Set-up callback functions for webpic
|
||||||
|
wpd.set_success_callback(success)
|
||||||
|
wpd.set_failure_callback(failed)
|
||||||
|
|
||||||
|
# Ask for download
|
||||||
|
print("\nWelcome to WebPicDownloader!")
|
||||||
|
url = input("Website URL ? ")
|
||||||
|
name = input("Folder name ? ")
|
||||||
|
wpd.start_downloading(url, name) # Start downloading
|
||||||
|
time.sleep(1) # We wait for the download to start before ask to stop it
|
||||||
|
wpd.stop_downloading(block=True) # Stop downloading but block till the download end.
|
37
webpicdownloader/util/UpdateUtils.py
Normal file
37
webpicdownloader/util/UpdateUtils.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from re import fullmatch, Pattern
|
||||||
|
from urllib import request as http
|
||||||
|
from urllib.error import HTTPError
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_version(url: str) -> str:
|
||||||
|
"""
|
||||||
|
Retrieve the latest webpicdownloader version available.
|
||||||
|
This method, only accept 200 http response.
|
||||||
|
|
||||||
|
* :url: -> Url of the "VERSION" file on the repository.
|
||||||
|
* RETURN -> the latests 'VERSION' file content.
|
||||||
|
* THROWABLE -> can raise HTTP or URL error see urllib doc.
|
||||||
|
"""
|
||||||
|
request = http.Request(
|
||||||
|
url=url,
|
||||||
|
headers={'User-Agent': "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/604.1"},
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
response = http.urlopen(request)
|
||||||
|
if response.getcode() != 200:
|
||||||
|
raise HTTPError("Bad response returned by server")
|
||||||
|
return response.read().decode('utf-8')
|
||||||
|
|
||||||
|
def check_for_update(version_current: str, version_url: str, version_pattern: Pattern) -> bool:
|
||||||
|
"""
|
||||||
|
Verify if a new version is available.
|
||||||
|
|
||||||
|
* :current: -> Current version
|
||||||
|
* :version_url: -> Url of the VERSION file on the repository.
|
||||||
|
* :version_pattern: -> Patter to match version retrieved with the version_url.
|
||||||
|
* RETURN -> True means a new version is available, False else.
|
||||||
|
"""
|
||||||
|
version = fetch_version(version_url)
|
||||||
|
if not fullmatch(version_pattern, version):
|
||||||
|
raise ValueError('The version retrieved from the remote server is not valid')
|
||||||
|
return version_current != version
|
145
webpicdownloader/view/HomeView.py
Normal file
145
webpicdownloader/view/HomeView.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import tkinter as tk
|
||||||
|
import tkinter.font as tfont
|
||||||
|
from tkinter import ttk
|
||||||
|
from tkinter import scrolledtext as tst
|
||||||
|
from webpicdownloader.controller.HomeController import HomeController
|
||||||
|
from webpicdownloader.view.MainWindow import MainWindow
|
||||||
|
|
||||||
|
|
||||||
|
class HomeView(ttk.Frame):
|
||||||
|
"""
|
||||||
|
View - HomeWindow
|
||||||
|
|
||||||
|
This view allows you to start the scraping/downloading process,
|
||||||
|
as well as to display the progress of the process.
|
||||||
|
|
||||||
|
@author Jérémi Nihart / EndMove
|
||||||
|
@link https://git.endmove.eu/EndMove/WebPicDownloader
|
||||||
|
@version 1.0.1
|
||||||
|
@since 2022-09-05
|
||||||
|
"""
|
||||||
|
# Variables
|
||||||
|
__controller: HomeController = None
|
||||||
|
|
||||||
|
# Constructor
|
||||||
|
def __init__(self, parent: MainWindow, controller: HomeController):
|
||||||
|
"""
|
||||||
|
Constructor
|
||||||
|
|
||||||
|
* :parent: -> The main windows container.
|
||||||
|
* :controller: -> The view controller
|
||||||
|
"""
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
# Init view
|
||||||
|
self.__init_content()
|
||||||
|
|
||||||
|
# Save and setup controller
|
||||||
|
self.__controller = controller
|
||||||
|
controller.set_view(self)
|
||||||
|
|
||||||
|
# START Internal functions
|
||||||
|
def __init_content(self) -> None:
|
||||||
|
"""
|
||||||
|
[internal function]
|
||||||
|
=> Initialize the view content.
|
||||||
|
"""
|
||||||
|
self.columnconfigure(0, weight=1)
|
||||||
|
self.columnconfigure(1, weight=3)
|
||||||
|
|
||||||
|
# Website link
|
||||||
|
self.web_label = ttk.Label(self, text="Website URL:")
|
||||||
|
self.web_label.grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
|
||||||
|
self.web_entry = ttk.Entry(self, width=50) # show="-"
|
||||||
|
self.web_entry.grid(row=0, column=1, sticky=tk.E, padx=5, pady=5, ipadx=2, ipady=2)
|
||||||
|
|
||||||
|
# Download name
|
||||||
|
self.name_label = ttk.Label(self, text="Download Name:")
|
||||||
|
self.name_label.grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
|
||||||
|
self.name_entry = ttk.Entry(self, width=50)
|
||||||
|
self.name_entry.grid(row=1, column=1, sticky=tk.E, padx=5, pady=5, ipadx=2, ipady=2)
|
||||||
|
|
||||||
|
# Logs area
|
||||||
|
log_textarea_font = tfont.Font(size=10)
|
||||||
|
self.log_textarea = tst.ScrolledText(self, font=log_textarea_font, wrap=tk.WORD, state=tk.DISABLED, width=40, height=8)#, font=("Times New Roman", 15))
|
||||||
|
self.log_textarea.grid(row=3, column=0, columnspan=2, sticky=tk.EW, pady=10, padx=10)
|
||||||
|
|
||||||
|
# Message state
|
||||||
|
self.message_label = ttk.Label(self, text='message label')
|
||||||
|
|
||||||
|
# Download button
|
||||||
|
self.download_button = ttk.Button(self, text="Start downloading", command=self.__event_button_download)
|
||||||
|
self.download_button.grid(row=5, column=1, sticky=tk.E, padx=5, pady=5, ipadx=5, ipady=2)
|
||||||
|
|
||||||
|
def __event_button_download(self) -> None:
|
||||||
|
"""
|
||||||
|
[internal function]
|
||||||
|
=> Function called when a download is requested.
|
||||||
|
"""
|
||||||
|
self.__controller.on_download_requested(self.web_entry.get(), self.name_entry.get())
|
||||||
|
# END Internal functions
|
||||||
|
|
||||||
|
# START Controller methods
|
||||||
|
def add_log(self, line: str) -> None:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Add a log in the textarea where the logs are displayed.
|
||||||
|
|
||||||
|
* :line: -> Log message to add.
|
||||||
|
"""
|
||||||
|
self.log_textarea.configure(state=tk.NORMAL)
|
||||||
|
self.log_textarea.insert(tk.END, f"~ {line}\n")
|
||||||
|
self.log_textarea.see(tk.END)
|
||||||
|
self.log_textarea.configure(state=tk.DISABLED)
|
||||||
|
|
||||||
|
def clear_logs(self) -> None:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Clean the textarea where the logs are displayed.
|
||||||
|
"""
|
||||||
|
self.log_textarea.configure(state=tk.NORMAL)
|
||||||
|
self.log_textarea.delete('1.0', tk.END)
|
||||||
|
self.log_textarea.configure(state=tk.DISABLED)
|
||||||
|
|
||||||
|
def show_error_message(self, message) -> None:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Display an error message on the interface.
|
||||||
|
|
||||||
|
* :message: -> Message to display.
|
||||||
|
"""
|
||||||
|
self.message_label.configure(text=message, foreground='red')
|
||||||
|
self.message_label.grid(row=4, column=0, columnspan=2, sticky=tk.NS, padx=2, pady=2)
|
||||||
|
self.message_label.after(25000, self.hide_message)
|
||||||
|
|
||||||
|
def show_success_message(self, message) -> None:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Display a success message on the interface.
|
||||||
|
|
||||||
|
* :message: -> Message to display.
|
||||||
|
"""
|
||||||
|
self.message_label.configure(text=message, foreground='green')
|
||||||
|
self.message_label.grid(row=4, column=0, columnspan=2, sticky=tk.NS, padx=2, pady=2)
|
||||||
|
self.message_label.after(25000, self.hide_message)
|
||||||
|
|
||||||
|
def hide_message(self) -> None:
|
||||||
|
"""
|
||||||
|
[function for controller and this view]
|
||||||
|
=> Hide the message on the interface.
|
||||||
|
"""
|
||||||
|
self.message_label.grid_forget()
|
||||||
|
|
||||||
|
def set_interface_state(self, disable: bool) -> None:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Allows to change the status of the interface with which the user
|
||||||
|
interacts by activating/deactivating it.
|
||||||
|
|
||||||
|
* :disabled: -> True: interface disabled, False: interface enabled.
|
||||||
|
"""
|
||||||
|
state = tk.DISABLED if disable else tk.NORMAL
|
||||||
|
self.web_entry['state'] = state
|
||||||
|
self.name_entry['state'] = state
|
||||||
|
self.download_button['state'] = state
|
||||||
|
# END Controller methods
|
101
webpicdownloader/view/InfoView.py
Normal file
101
webpicdownloader/view/InfoView.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import tkinter as tk
|
||||||
|
from tkinter import font
|
||||||
|
from tkinter import ttk
|
||||||
|
from webpicdownloader.controller.Frames import Frames
|
||||||
|
from webpicdownloader.controller.InfoController import InfoController
|
||||||
|
from webpicdownloader.view.MainWindow import MainWindow
|
||||||
|
|
||||||
|
|
||||||
|
class InfoView(ttk.Frame):
|
||||||
|
"""
|
||||||
|
View - InfoWindow
|
||||||
|
|
||||||
|
This view displays information about the program, as well as its version and release date.
|
||||||
|
|
||||||
|
@author Jérémi Nihart / EndMove
|
||||||
|
@link https://git.endmove.eu/EndMove/WebPicDownloader
|
||||||
|
@version 1.0.0
|
||||||
|
@since 2022-09-06
|
||||||
|
"""
|
||||||
|
# Variables
|
||||||
|
__controller: InfoController = None
|
||||||
|
|
||||||
|
# Constructor
|
||||||
|
def __init__(self, parent: MainWindow, controller: InfoController):
|
||||||
|
"""
|
||||||
|
Constructor
|
||||||
|
|
||||||
|
* :parent: -> The main windows container.
|
||||||
|
* :controller: -> The view controller
|
||||||
|
"""
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
# Init view
|
||||||
|
self.__init_content()
|
||||||
|
|
||||||
|
# Save and setup controller
|
||||||
|
self.__controller = controller
|
||||||
|
controller.set_view(self)
|
||||||
|
|
||||||
|
# START Internal functions
|
||||||
|
def __init_content(self) -> None:
|
||||||
|
"""
|
||||||
|
[internal function]
|
||||||
|
=> Initialize the view content.
|
||||||
|
"""
|
||||||
|
self.columnconfigure(0, weight=4)
|
||||||
|
|
||||||
|
# Back button
|
||||||
|
self.back_button = ttk.Button(self, text="Back", command=self.__event_button_back)
|
||||||
|
self.back_button.grid(row=0, column=0, sticky=tk.E, padx=5, pady=5, ipadx=1, ipady=1)
|
||||||
|
|
||||||
|
# About title
|
||||||
|
self.title_label_font = font.Font(self, size=16, weight=font.BOLD)
|
||||||
|
self.title_label = ttk.Label(self, text="A title", font=self.title_label_font)
|
||||||
|
self.title_label.grid(row=1, column=0, sticky=tk.NS, padx=2, pady=2)
|
||||||
|
|
||||||
|
# About content
|
||||||
|
self.content_label_font = font.Font(self, size=10)
|
||||||
|
self.content_label = ttk.Label(self, wraplength=400, justify='center', text='A long text', font=self.content_label_font, foreground='blue')
|
||||||
|
self.content_label.grid(row=2, column=0, sticky=tk.NS)
|
||||||
|
|
||||||
|
# About version
|
||||||
|
self.version_label = ttk.Label(self, text='version : 1.0.0 - 02-02-2022')
|
||||||
|
self.version_label.grid(row=3, column=0, sticky=tk.NS, pady=15)
|
||||||
|
|
||||||
|
def __event_button_back(self) -> None:
|
||||||
|
"""
|
||||||
|
[internal function]
|
||||||
|
=> Function called when back button pressed.
|
||||||
|
"""
|
||||||
|
self.__controller.on_change_view(Frames.HOME)
|
||||||
|
# END Internal functions
|
||||||
|
|
||||||
|
# START Controller methods
|
||||||
|
def set_title(self, title: str) -> None:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Define view/page info : title
|
||||||
|
|
||||||
|
* :title: -> Title for the view.
|
||||||
|
"""
|
||||||
|
self.title_label.configure(text=title)
|
||||||
|
|
||||||
|
def set_content(self, content: str) -> None:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Define view/page info : content
|
||||||
|
|
||||||
|
* :content: -> Content for the view.
|
||||||
|
"""
|
||||||
|
self.content_label.configure(text=content)
|
||||||
|
|
||||||
|
def set_version(self, version: str) -> None:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Define view/page info : version
|
||||||
|
|
||||||
|
* :version: -> Version for the view.
|
||||||
|
"""
|
||||||
|
self.version_label.configure(text=version)
|
||||||
|
# END Controller methods
|
154
webpicdownloader/view/MainWindow.py
Normal file
154
webpicdownloader/view/MainWindow.py
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import tkinter as tk
|
||||||
|
from tkinter import messagebox
|
||||||
|
from webpicdownloader.controller.Frames import Frames
|
||||||
|
from webpicdownloader.controller.MainController import MainController
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(tk.Tk):
|
||||||
|
"""
|
||||||
|
View - MainWindow
|
||||||
|
|
||||||
|
This view is the main view of the application, it manages the different frames/views
|
||||||
|
of the application, captures the events to send them to the main controller.
|
||||||
|
|
||||||
|
@author Jérémi Nihart / EndMove
|
||||||
|
@link https://git.endmove.eu/EndMove/WebPicDownloader
|
||||||
|
@version 1.0.1
|
||||||
|
@since 2022-09-04
|
||||||
|
"""
|
||||||
|
# Variables
|
||||||
|
__controller: MainController = None
|
||||||
|
__views: dict = None
|
||||||
|
__frame_id: int = None
|
||||||
|
|
||||||
|
# Constructor
|
||||||
|
def __init__(self, controller: MainController) -> None:
|
||||||
|
"""
|
||||||
|
Constructor
|
||||||
|
|
||||||
|
* :controller: -> The main application cpntroller.
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
# Init view repository
|
||||||
|
self.__views = {}
|
||||||
|
|
||||||
|
# Save and setup main controller
|
||||||
|
self.__controller = controller
|
||||||
|
controller.set_view(self)
|
||||||
|
|
||||||
|
# Init view components & more
|
||||||
|
self.__init_window()
|
||||||
|
self.__init_top_menu()
|
||||||
|
self.__init_bind_protocol()
|
||||||
|
|
||||||
|
# START Internal methods
|
||||||
|
def __init_window(self) -> None:
|
||||||
|
"""
|
||||||
|
[internal function]
|
||||||
|
=> Initialize window parameters
|
||||||
|
"""
|
||||||
|
self.iconbitmap(f"{self.__controller.get_config('sys_directory')}\\webpicdownloader\\assets\\logo.ico") # App logo
|
||||||
|
window_width = 430 # App width
|
||||||
|
window_height = 305 # App height
|
||||||
|
x_cordinate = int((self.winfo_screenwidth()/2) - (window_width/2))
|
||||||
|
y_cordinate = int((self.winfo_screenheight()/2) - (window_height/2))
|
||||||
|
self.geometry(f"{window_width}x{window_height}+{x_cordinate}+{y_cordinate}") # App size and middle centering
|
||||||
|
self.resizable(False, False) # Disable app resizing
|
||||||
|
|
||||||
|
def __init_top_menu(self) -> None:
|
||||||
|
"""
|
||||||
|
[internal function]
|
||||||
|
=> Initialize top menu of the window.
|
||||||
|
"""
|
||||||
|
main_menu = tk.Menu(self)
|
||||||
|
|
||||||
|
# Top menu File item
|
||||||
|
col1_menu = tk.Menu(main_menu, tearoff=0)
|
||||||
|
col1_menu.add_command(label="Open app folder", command=self.__controller.on_open_folder)
|
||||||
|
col1_menu.add_separator()
|
||||||
|
col1_menu.add_command(label="Quit", command=self.__controller.on_quite)
|
||||||
|
main_menu.add_cascade(label="File", menu=col1_menu)
|
||||||
|
|
||||||
|
# Top menu Help item
|
||||||
|
col2_menu = tk.Menu(main_menu, tearoff=0)
|
||||||
|
col2_menu.add_command(label="Check for update", command=self.__controller.on_check_for_update)
|
||||||
|
col2_menu.add_command(label="About", command=self.__controller.on_about)
|
||||||
|
main_menu.add_cascade(label="Help", menu=col2_menu)
|
||||||
|
|
||||||
|
self.config(menu=main_menu)
|
||||||
|
|
||||||
|
def __init_bind_protocol(self) -> None:
|
||||||
|
"""
|
||||||
|
[internal function]
|
||||||
|
=> Initialize the function bindding on events of the main window.
|
||||||
|
"""
|
||||||
|
self.protocol("WM_DELETE_WINDOW", self.__controller.on_quite)
|
||||||
|
# END Internal methods
|
||||||
|
|
||||||
|
# START App methods
|
||||||
|
def add_view(self, frame, view) -> None:
|
||||||
|
"""
|
||||||
|
[function for app]
|
||||||
|
|
||||||
|
* :frame: -> the frame id of the view to add.
|
||||||
|
"""
|
||||||
|
self.__views[frame] = view
|
||||||
|
# END App methods
|
||||||
|
|
||||||
|
# START Controller methods
|
||||||
|
def set_window_title(self, title: str) -> None:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Sets the title of the main window.
|
||||||
|
|
||||||
|
* :title: -> Window title.
|
||||||
|
"""
|
||||||
|
self.title(title)
|
||||||
|
|
||||||
|
def show_frame(self, frame: Frames) -> None:
|
||||||
|
"""
|
||||||
|
[function for app & controller]
|
||||||
|
=> Allows to display the selected frame provided that it
|
||||||
|
has been previously added to the frame dictionary.
|
||||||
|
|
||||||
|
* :frame: -> the frame if of the view to display.
|
||||||
|
"""
|
||||||
|
if self.__views.get(frame):
|
||||||
|
if self.__frame_id:
|
||||||
|
self.__views.get(self.__frame_id).pack_forget()
|
||||||
|
self.__views.get(frame).pack(fill=tk.BOTH, expand=False)
|
||||||
|
self.__frame_id = frame
|
||||||
|
else:
|
||||||
|
raise ValueError("Unable to find the requested Frame")
|
||||||
|
|
||||||
|
def close_window(self) -> None:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Closes the main window and stops the program from the controller.
|
||||||
|
"""
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
def show_question_dialog(self, title: str, message: str, icon: str='question') -> bool:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Display a question dialog to the user, which he can answer with yes or no.
|
||||||
|
|
||||||
|
* :title: -> Title of the dialogue.
|
||||||
|
* :message: -> Message of the dialogue displayed to the user.
|
||||||
|
* :icon: -> Icon of the dialogue displayed to the user.
|
||||||
|
* RETURN -> True id the user selected yes, False else.
|
||||||
|
"""
|
||||||
|
return True if (messagebox.askquestion(title, message, icon=icon) == "yes") else False
|
||||||
|
|
||||||
|
def show_information_dialog(self, title: str, message: str, icon: str='info') -> None:
|
||||||
|
"""
|
||||||
|
[function for controller]
|
||||||
|
=> Display an information dialog to the user.
|
||||||
|
|
||||||
|
* :title: -> Title of the dialogue.
|
||||||
|
* :message: -> Message of the dialogue displayed to the user.
|
||||||
|
* :icon: -> Icon of the dialogue displayed to the user.
|
||||||
|
"""
|
||||||
|
messagebox.showinfo(title, message, icon=icon)
|
||||||
|
# END Controller methods
|
Reference in New Issue
Block a user