├── .dockerignore ├── .editorconfig ├── .env.example ├── .gitignore ├── .travis.yml ├── Dockerfile ├── README.md ├── bin ├── dev_setup.sh ├── docker_build.sh ├── docker_push.sh ├── flickr-cli └── phpstan.sh ├── composer.json ├── composer.lock ├── flickr-cli.sublime-project ├── phpcs.xml └── src └── TheFox ├── FlickrCli ├── Command │ ├── AlbumsCommand.php │ ├── AuthCommand.php │ ├── DeleteCommand.php │ ├── DownloadCommand.php │ ├── FilesCommand.php │ ├── FlickrCliCommand.php │ ├── PiwigoCommand.php │ └── UploadCommand.php ├── Exception │ └── SignalException.php ├── FlickrCli.php └── Service │ ├── AbstractService.php │ └── ApiService.php └── OAuth ├── Common └── Http │ └── Client │ └── GuzzleStreamClient.php └── OAuth1 └── Service └── Flickr.php /.dockerignore: -------------------------------------------------------------------------------- 1 | /log 2 | /vendor 3 | /tmp 4 | *.txt 5 | *.yml 6 | *.sublime-project 7 | *.sublime-workspace 8 | .DS_Store 9 | .editorconfig 10 | .git 11 | .gitignore 12 | .idea 13 | .env 14 | .env.example 15 | README.md 16 | phpcs.xml 17 | 18 | .dockerignore 19 | Dockerfile 20 | 21 | *.lock1 22 | Dockerfile1 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = false 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | IMAGE_NAME="thefox21/flickr-cli" 2 | GITHUB_API_TOKEN="" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | CHANGELOG-*.txt 2 | config.yml 3 | composer.phar 4 | /vendor/ 5 | /tmp/ 6 | /log/ 7 | .env 8 | .idea 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 7.0 4 | - 7.1 5 | - 7.2 6 | install: 7 | - composer install --prefer-source --no-interaction 8 | before_script: 9 | - phpenv rehash 10 | script: 11 | - ./bin/phpstan.sh 12 | - ./vendor/bin/phpcs --config-set ignore_warnings_on_exit 1 13 | - ./vendor/bin/phpcs --config-show 14 | - ./vendor/bin/phpcs 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.2-rc-cli 2 | ARG COMPOSER_AUTH 3 | 4 | ENV DEBIAN_FRONTEND noninteractive 5 | ENV FLICKRCLI_CONFIG /data/config.yml 6 | 7 | RUN apt-get update && \ 8 | apt-get install -y apt-transport-https build-essential curl libcurl3 libcurl4-openssl-dev libicu-dev zlib1g-dev libxml2-dev && \ 9 | docker-php-ext-install curl xml zip bcmath pcntl && \ 10 | apt-get clean 11 | 12 | # Install Composer. 13 | COPY --from=composer:1.5 /usr/bin/composer /usr/bin/composer 14 | 15 | # Root App folder 16 | RUN mkdir /app 17 | WORKDIR /app 18 | ADD . /app 19 | 20 | # Install dependencies. 21 | RUN composer install --no-dev --optimize-autoloader --no-progress --no-suggest --no-interaction 22 | 23 | RUN ls -la 24 | 25 | RUN rm -r /root/.composer/* /root/.composer 26 | RUN ls -la /root 27 | 28 | # Use to store the config inside a volume. 29 | RUN mkdir /data && chmod 777 /data 30 | VOLUME /data 31 | 32 | VOLUME /mnt 33 | WORKDIR /mnt 34 | 35 | ENTRYPOINT ["php", "/app/bin/flickr-cli"] 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlickrCLI 2 | 3 | A command-line interface to [Flickr](https://www.flickr.com/). Upload and download photos, photo sets, directories via shell. 4 | 5 | ## Installation 6 | 7 | 1. Clone from Github: 8 | 9 | git clone https://github.com/TheFox/flickr-cli.git 10 | 11 | 2. Install dependencies: 12 | 13 | composer install 14 | 15 | 3. Go to to create a new API key. 16 | The first time you run `./bin/flickr-cli auth` you'll be prompted to enter your new consumer key and secret. 17 | 18 | ## Usage 19 | 20 | First, get the access token: 21 | 22 | ./bin/flickr-cli auth 23 | 24 | ### Upload 25 | 26 | ./bin/flickr-cli upload [-d DESCRIPTION] [-t TAG,...] [-s SET,...] DIRECTORY... 27 | 28 | ### Download 29 | 30 | ./bin/flickr-cli download -d DIRECTORY [SET...] 31 | 32 | To download all photosets to directory `photosets`: 33 | 34 | ./bin/flickr-cli download -d photosets 35 | 36 | Or to download only the photoset *Holiday 2013*: 37 | 38 | ./bin/flickr-cli download -d photosets 'Holiday 2013' 39 | 40 | To download all photos into directories named by photo ID 41 | (and so which will not change when you rename albums or photos; perfect for a complete Flickr backup) 42 | you can use the `--id-dirs` option: 43 | 44 | ./bin/flickr-cli download -d flickr_backup --id-dirs 45 | 46 | This creates a stable directory structure of the form `destination_dir/hash/hash/photo-ID/` 47 | and saves the full original photo file along with a `metadata.yml` file containing all photo metadata. 48 | The hashes, which are the first two sets of two characters of the MD5 hash of the ID, 49 | are required in order to prevent a single directory from containing too many subdirectories 50 | (to avoid problems with some filesystems). 51 | 52 | ## Usage of the Docker Image 53 | 54 | ### Setup 55 | 56 | To use this software within Docker follow this steps. 57 | 58 | 1. Create a volume. This is used to store the configuration file for the `auth` step. 59 | 60 | docker volume create flickrcli 61 | 62 | 2. Get the access token (it will create `config.yml` file in the volume). 63 | 64 | docker run --rm -it -u $(id -u):$(id -g) -v "$PWD":/mnt -v flickrcli:/data thefox21/flickr-cli auth 65 | 66 | or you can store the `config.yml` in your `$HOME/.flickr-cli` directory and use: 67 | 68 | mkdir $HOME/.flickr-cli 69 | docker run --rm -it -u $(id -u):$(id -g) -v "$PWD":/mnt -v "$HOME/.flickr-cli":/data thefox21/flickr-cli auth 70 | 71 | ### Usage 72 | 73 | Upload directory `2017.06.01-Spindleruv_mlyn` full of JPEGs to Flickr: 74 | 75 | docker run --rm -it -u $(id -u):$(id -g) -v "$PWD":/mnt -v flickrcli:/data thefox21/flickr-cli upload --config=/data/config.yml --tags "2017.06.01 Spindleruv_mlyn" --sets "2017.06.01-Spindleruv_mlyn" 2017.06.01-Spindleruv_mlyn 76 | 77 | For Docker image troubleshooting you can use: 78 | 79 | docker run --rm -it -u $(id -u):$(id -g) -v "$PWD":/mnt -v flickrcli:/data --entrypoint=/bin/bash thefox21/flickr-cli 80 | 81 | ### Paths 82 | 83 | - `/app` - Main Application directory. 84 | - `/data` - Volume for variable data. 85 | - `/mnt` - Host system's `$PWD`. 86 | 87 | ## Documentations 88 | 89 | - [Flickr API documentation](http://www.flickr.com/services/api/) 90 | - [Docker documentation](https://docs.docker.com/) 91 | 92 | ## License 93 | 94 | Copyright (C) 2016 Christian Mayer 95 | 96 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 97 | 98 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . 99 | -------------------------------------------------------------------------------- /bin/dev_setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SCRIPT_BASEDIR=$(dirname "$0") 4 | 5 | 6 | set -e 7 | cd "${SCRIPT_BASEDIR}/.." 8 | 9 | which php &> /dev/null || { echo 'ERROR: php not found in PATH'; exit 1; } 10 | which composer &> /dev/null || { echo 'ERROR: composer not found in PATH'; exit 1; } 11 | 12 | if [[ ! -f .env ]]; then 13 | cp .env.example .env 14 | fi 15 | 16 | composer install --no-interaction 17 | -------------------------------------------------------------------------------- /bin/docker_build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Builds the Docker images. 4 | 5 | DATE=$(date +"%Y%m%d_%H%M%S") 6 | SCRIPT_BASEDIR=$(dirname "$0") 7 | 8 | 9 | set -e 10 | which docker &> /dev/null || { echo 'ERROR: docker not found in PATH'; exit 1; } 11 | which sed &> /dev/null || { echo 'ERROR: sed not found in PATH'; exit 1; } 12 | 13 | cd "${SCRIPT_BASEDIR}/.." 14 | source ./.env 15 | 16 | docker build --tag ${IMAGE_NAME}:${DATE} --build-arg COMPOSER_AUTH="{\"github.com\":\"$GITHUB_API_TOKEN\"}" . 17 | docker tag ${IMAGE_NAME}:${DATE} ${IMAGE_NAME}:latest 18 | -------------------------------------------------------------------------------- /bin/docker_push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Tags the existing Docker image and pushes the image to the Hub. 4 | 5 | # Example usage, to tag Version 1.2.3: 6 | # ./bin/docker_push.sh latest 1 1.2 1.2.3 7 | 8 | # Example usage, to tag Version 1.2.0-dev.4: 9 | # ./bin/docker_push.sh dev 2-dev 1.2-dev 1.2.0-dev 1.2.0-dev.4 10 | 11 | DATE=$(date +"%Y%m%d_%H%M%S") 12 | SCRIPT_BASEDIR=$(dirname "$0") 13 | versions=$* 14 | 15 | 16 | set -e 17 | which docker &> /dev/null || { echo 'ERROR: docker not found in PATH'; exit 1; } 18 | 19 | cd "${SCRIPT_BASEDIR}/.." 20 | source ./.env 21 | 22 | if [[ -z "$versions" ]]; then 23 | echo 'ERROR: no version given' 24 | exit 1 25 | fi 26 | 27 | for version in $versions ; do 28 | echo "Tag version: $version" 29 | 30 | # Tag 31 | docker tag ${IMAGE_NAME}:latest ${IMAGE_NAME}:${version} 32 | 33 | # Push Tags 34 | docker push ${IMAGE_NAME}:${version} 35 | done 36 | -------------------------------------------------------------------------------- /bin/flickr-cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new AlbumsCommand()); 34 | $application->add(new AuthCommand()); 35 | $application->add(new DeleteCommand()); 36 | $application->add(new DownloadCommand()); 37 | $application->add(new FilesCommand()); 38 | $application->add(new UploadCommand()); 39 | $application->add(new PiwigoCommand()); 40 | $application->run(); 41 | -------------------------------------------------------------------------------- /bin/phpstan.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SCRIPT_BASEDIR=$(dirname "$0") 4 | 5 | 6 | set -e 7 | cd "${SCRIPT_BASEDIR}/.." 8 | 9 | vendor/bin/phpstan analyse --no-progress --level 5 src bin/flickr-cli 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thefox/flickr-cli", 3 | "description": "Upload and download Flickr photos, photo sets, directories via shell.", 4 | "license": "GPL-3.0", 5 | "type": "project", 6 | "keywords": [ 7 | "Flickr", 8 | "Upload", 9 | "Download", 10 | "Photo", 11 | "CLI" 12 | ], 13 | "homepage": "https://github.com/TheFox/flickr-cli", 14 | "authors": [ 15 | { 16 | "name": "Christian Mayer", 17 | "email": "christian@fox21.at", 18 | "homepage": "https://fox21.at" 19 | } 20 | ], 21 | "require": { 22 | "php": "^7.0", 23 | "rezzza/flickr": "^1.1", 24 | "symfony/yaml": "^2.3", 25 | "symfony/console": "^3.1", 26 | "symfony/filesystem": "^3.1", 27 | "symfony/finder": "^3.1", 28 | "monolog/monolog": "^1.21", 29 | "guzzlehttp/guzzle": "^3.8", 30 | "lusitanian/oauth": "^0.2", 31 | "rych/bytesize": "^1.0", 32 | "doctrine/dbal": "^2.5" 33 | }, 34 | "require-dev": { 35 | "phpstan/phpstan": "^0.7", 36 | "squizlabs/php_codesniffer": "^3.0" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "": "src" 41 | } 42 | }, 43 | "bin": [ 44 | "bin/flickr-cli" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "99ac10d80de82846470141b3fe88513e", 8 | "packages": [ 9 | { 10 | "name": "doctrine/annotations", 11 | "version": "v1.4.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/doctrine/annotations.git", 15 | "reference": "54cacc9b81758b14e3ce750f205a393d52339e97" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97", 20 | "reference": "54cacc9b81758b14e3ce750f205a393d52339e97", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "doctrine/lexer": "1.*", 25 | "php": "^5.6 || ^7.0" 26 | }, 27 | "require-dev": { 28 | "doctrine/cache": "1.*", 29 | "phpunit/phpunit": "^5.7" 30 | }, 31 | "type": "library", 32 | "extra": { 33 | "branch-alias": { 34 | "dev-master": "1.4.x-dev" 35 | } 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" 40 | } 41 | }, 42 | "notification-url": "https://packagist.org/downloads/", 43 | "license": [ 44 | "MIT" 45 | ], 46 | "authors": [ 47 | { 48 | "name": "Roman Borschel", 49 | "email": "roman@code-factory.org" 50 | }, 51 | { 52 | "name": "Benjamin Eberlei", 53 | "email": "kontakt@beberlei.de" 54 | }, 55 | { 56 | "name": "Guilherme Blanco", 57 | "email": "guilhermeblanco@gmail.com" 58 | }, 59 | { 60 | "name": "Jonathan Wage", 61 | "email": "jonwage@gmail.com" 62 | }, 63 | { 64 | "name": "Johannes Schmitt", 65 | "email": "schmittjoh@gmail.com" 66 | } 67 | ], 68 | "description": "Docblock Annotations Parser", 69 | "homepage": "http://www.doctrine-project.org", 70 | "keywords": [ 71 | "annotations", 72 | "docblock", 73 | "parser" 74 | ], 75 | "time": "2017-02-24T16:22:25+00:00" 76 | }, 77 | { 78 | "name": "doctrine/cache", 79 | "version": "v1.6.2", 80 | "source": { 81 | "type": "git", 82 | "url": "https://github.com/doctrine/cache.git", 83 | "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b" 84 | }, 85 | "dist": { 86 | "type": "zip", 87 | "url": "https://api.github.com/repos/doctrine/cache/zipball/eb152c5100571c7a45470ff2a35095ab3f3b900b", 88 | "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b", 89 | "shasum": "" 90 | }, 91 | "require": { 92 | "php": "~5.5|~7.0" 93 | }, 94 | "conflict": { 95 | "doctrine/common": ">2.2,<2.4" 96 | }, 97 | "require-dev": { 98 | "phpunit/phpunit": "~4.8|~5.0", 99 | "predis/predis": "~1.0", 100 | "satooshi/php-coveralls": "~0.6" 101 | }, 102 | "type": "library", 103 | "extra": { 104 | "branch-alias": { 105 | "dev-master": "1.6.x-dev" 106 | } 107 | }, 108 | "autoload": { 109 | "psr-4": { 110 | "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" 111 | } 112 | }, 113 | "notification-url": "https://packagist.org/downloads/", 114 | "license": [ 115 | "MIT" 116 | ], 117 | "authors": [ 118 | { 119 | "name": "Roman Borschel", 120 | "email": "roman@code-factory.org" 121 | }, 122 | { 123 | "name": "Benjamin Eberlei", 124 | "email": "kontakt@beberlei.de" 125 | }, 126 | { 127 | "name": "Guilherme Blanco", 128 | "email": "guilhermeblanco@gmail.com" 129 | }, 130 | { 131 | "name": "Jonathan Wage", 132 | "email": "jonwage@gmail.com" 133 | }, 134 | { 135 | "name": "Johannes Schmitt", 136 | "email": "schmittjoh@gmail.com" 137 | } 138 | ], 139 | "description": "Caching library offering an object-oriented API for many cache backends", 140 | "homepage": "http://www.doctrine-project.org", 141 | "keywords": [ 142 | "cache", 143 | "caching" 144 | ], 145 | "time": "2017-07-22T12:49:21+00:00" 146 | }, 147 | { 148 | "name": "doctrine/collections", 149 | "version": "v1.4.0", 150 | "source": { 151 | "type": "git", 152 | "url": "https://github.com/doctrine/collections.git", 153 | "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba" 154 | }, 155 | "dist": { 156 | "type": "zip", 157 | "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba", 158 | "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba", 159 | "shasum": "" 160 | }, 161 | "require": { 162 | "php": "^5.6 || ^7.0" 163 | }, 164 | "require-dev": { 165 | "doctrine/coding-standard": "~0.1@dev", 166 | "phpunit/phpunit": "^5.7" 167 | }, 168 | "type": "library", 169 | "extra": { 170 | "branch-alias": { 171 | "dev-master": "1.3.x-dev" 172 | } 173 | }, 174 | "autoload": { 175 | "psr-0": { 176 | "Doctrine\\Common\\Collections\\": "lib/" 177 | } 178 | }, 179 | "notification-url": "https://packagist.org/downloads/", 180 | "license": [ 181 | "MIT" 182 | ], 183 | "authors": [ 184 | { 185 | "name": "Roman Borschel", 186 | "email": "roman@code-factory.org" 187 | }, 188 | { 189 | "name": "Benjamin Eberlei", 190 | "email": "kontakt@beberlei.de" 191 | }, 192 | { 193 | "name": "Guilherme Blanco", 194 | "email": "guilhermeblanco@gmail.com" 195 | }, 196 | { 197 | "name": "Jonathan Wage", 198 | "email": "jonwage@gmail.com" 199 | }, 200 | { 201 | "name": "Johannes Schmitt", 202 | "email": "schmittjoh@gmail.com" 203 | } 204 | ], 205 | "description": "Collections Abstraction library", 206 | "homepage": "http://www.doctrine-project.org", 207 | "keywords": [ 208 | "array", 209 | "collections", 210 | "iterator" 211 | ], 212 | "time": "2017-01-03T10:49:41+00:00" 213 | }, 214 | { 215 | "name": "doctrine/common", 216 | "version": "v2.7.3", 217 | "source": { 218 | "type": "git", 219 | "url": "https://github.com/doctrine/common.git", 220 | "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9" 221 | }, 222 | "dist": { 223 | "type": "zip", 224 | "url": "https://api.github.com/repos/doctrine/common/zipball/4acb8f89626baafede6ee5475bc5844096eba8a9", 225 | "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9", 226 | "shasum": "" 227 | }, 228 | "require": { 229 | "doctrine/annotations": "1.*", 230 | "doctrine/cache": "1.*", 231 | "doctrine/collections": "1.*", 232 | "doctrine/inflector": "1.*", 233 | "doctrine/lexer": "1.*", 234 | "php": "~5.6|~7.0" 235 | }, 236 | "require-dev": { 237 | "phpunit/phpunit": "^5.4.6" 238 | }, 239 | "type": "library", 240 | "extra": { 241 | "branch-alias": { 242 | "dev-master": "2.7.x-dev" 243 | } 244 | }, 245 | "autoload": { 246 | "psr-4": { 247 | "Doctrine\\Common\\": "lib/Doctrine/Common" 248 | } 249 | }, 250 | "notification-url": "https://packagist.org/downloads/", 251 | "license": [ 252 | "MIT" 253 | ], 254 | "authors": [ 255 | { 256 | "name": "Roman Borschel", 257 | "email": "roman@code-factory.org" 258 | }, 259 | { 260 | "name": "Benjamin Eberlei", 261 | "email": "kontakt@beberlei.de" 262 | }, 263 | { 264 | "name": "Guilherme Blanco", 265 | "email": "guilhermeblanco@gmail.com" 266 | }, 267 | { 268 | "name": "Jonathan Wage", 269 | "email": "jonwage@gmail.com" 270 | }, 271 | { 272 | "name": "Johannes Schmitt", 273 | "email": "schmittjoh@gmail.com" 274 | } 275 | ], 276 | "description": "Common Library for Doctrine projects", 277 | "homepage": "http://www.doctrine-project.org", 278 | "keywords": [ 279 | "annotations", 280 | "collections", 281 | "eventmanager", 282 | "persistence", 283 | "spl" 284 | ], 285 | "time": "2017-07-22T08:35:12+00:00" 286 | }, 287 | { 288 | "name": "doctrine/dbal", 289 | "version": "v2.5.13", 290 | "source": { 291 | "type": "git", 292 | "url": "https://github.com/doctrine/dbal.git", 293 | "reference": "729340d8d1eec8f01bff708e12e449a3415af873" 294 | }, 295 | "dist": { 296 | "type": "zip", 297 | "url": "https://api.github.com/repos/doctrine/dbal/zipball/729340d8d1eec8f01bff708e12e449a3415af873", 298 | "reference": "729340d8d1eec8f01bff708e12e449a3415af873", 299 | "shasum": "" 300 | }, 301 | "require": { 302 | "doctrine/common": ">=2.4,<2.8-dev", 303 | "php": ">=5.3.2" 304 | }, 305 | "require-dev": { 306 | "phpunit/phpunit": "4.*", 307 | "symfony/console": "2.*||^3.0" 308 | }, 309 | "suggest": { 310 | "symfony/console": "For helpful console commands such as SQL execution and import of files." 311 | }, 312 | "bin": [ 313 | "bin/doctrine-dbal" 314 | ], 315 | "type": "library", 316 | "extra": { 317 | "branch-alias": { 318 | "dev-master": "2.5.x-dev" 319 | } 320 | }, 321 | "autoload": { 322 | "psr-0": { 323 | "Doctrine\\DBAL\\": "lib/" 324 | } 325 | }, 326 | "notification-url": "https://packagist.org/downloads/", 327 | "license": [ 328 | "MIT" 329 | ], 330 | "authors": [ 331 | { 332 | "name": "Roman Borschel", 333 | "email": "roman@code-factory.org" 334 | }, 335 | { 336 | "name": "Benjamin Eberlei", 337 | "email": "kontakt@beberlei.de" 338 | }, 339 | { 340 | "name": "Guilherme Blanco", 341 | "email": "guilhermeblanco@gmail.com" 342 | }, 343 | { 344 | "name": "Jonathan Wage", 345 | "email": "jonwage@gmail.com" 346 | } 347 | ], 348 | "description": "Database Abstraction Layer", 349 | "homepage": "http://www.doctrine-project.org", 350 | "keywords": [ 351 | "database", 352 | "dbal", 353 | "persistence", 354 | "queryobject" 355 | ], 356 | "time": "2017-07-22T20:44:48+00:00" 357 | }, 358 | { 359 | "name": "doctrine/inflector", 360 | "version": "v1.2.0", 361 | "source": { 362 | "type": "git", 363 | "url": "https://github.com/doctrine/inflector.git", 364 | "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462" 365 | }, 366 | "dist": { 367 | "type": "zip", 368 | "url": "https://api.github.com/repos/doctrine/inflector/zipball/e11d84c6e018beedd929cff5220969a3c6d1d462", 369 | "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462", 370 | "shasum": "" 371 | }, 372 | "require": { 373 | "php": "^7.0" 374 | }, 375 | "require-dev": { 376 | "phpunit/phpunit": "^6.2" 377 | }, 378 | "type": "library", 379 | "extra": { 380 | "branch-alias": { 381 | "dev-master": "1.2.x-dev" 382 | } 383 | }, 384 | "autoload": { 385 | "psr-4": { 386 | "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector" 387 | } 388 | }, 389 | "notification-url": "https://packagist.org/downloads/", 390 | "license": [ 391 | "MIT" 392 | ], 393 | "authors": [ 394 | { 395 | "name": "Roman Borschel", 396 | "email": "roman@code-factory.org" 397 | }, 398 | { 399 | "name": "Benjamin Eberlei", 400 | "email": "kontakt@beberlei.de" 401 | }, 402 | { 403 | "name": "Guilherme Blanco", 404 | "email": "guilhermeblanco@gmail.com" 405 | }, 406 | { 407 | "name": "Jonathan Wage", 408 | "email": "jonwage@gmail.com" 409 | }, 410 | { 411 | "name": "Johannes Schmitt", 412 | "email": "schmittjoh@gmail.com" 413 | } 414 | ], 415 | "description": "Common String Manipulations with regard to casing and singular/plural rules.", 416 | "homepage": "http://www.doctrine-project.org", 417 | "keywords": [ 418 | "inflection", 419 | "pluralize", 420 | "singularize", 421 | "string" 422 | ], 423 | "time": "2017-07-22T12:18:28+00:00" 424 | }, 425 | { 426 | "name": "doctrine/lexer", 427 | "version": "v1.0.1", 428 | "source": { 429 | "type": "git", 430 | "url": "https://github.com/doctrine/lexer.git", 431 | "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c" 432 | }, 433 | "dist": { 434 | "type": "zip", 435 | "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c", 436 | "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c", 437 | "shasum": "" 438 | }, 439 | "require": { 440 | "php": ">=5.3.2" 441 | }, 442 | "type": "library", 443 | "extra": { 444 | "branch-alias": { 445 | "dev-master": "1.0.x-dev" 446 | } 447 | }, 448 | "autoload": { 449 | "psr-0": { 450 | "Doctrine\\Common\\Lexer\\": "lib/" 451 | } 452 | }, 453 | "notification-url": "https://packagist.org/downloads/", 454 | "license": [ 455 | "MIT" 456 | ], 457 | "authors": [ 458 | { 459 | "name": "Roman Borschel", 460 | "email": "roman@code-factory.org" 461 | }, 462 | { 463 | "name": "Guilherme Blanco", 464 | "email": "guilhermeblanco@gmail.com" 465 | }, 466 | { 467 | "name": "Johannes Schmitt", 468 | "email": "schmittjoh@gmail.com" 469 | } 470 | ], 471 | "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", 472 | "homepage": "http://www.doctrine-project.org", 473 | "keywords": [ 474 | "lexer", 475 | "parser" 476 | ], 477 | "time": "2014-09-09T13:34:57+00:00" 478 | }, 479 | { 480 | "name": "guzzlehttp/guzzle", 481 | "version": "v3.8.1", 482 | "source": { 483 | "type": "git", 484 | "url": "https://github.com/guzzle/guzzle.git", 485 | "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba" 486 | }, 487 | "dist": { 488 | "type": "zip", 489 | "url": "https://api.github.com/repos/guzzle/guzzle/zipball/4de0618a01b34aa1c8c33a3f13f396dcd3882eba", 490 | "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba", 491 | "shasum": "" 492 | }, 493 | "require": { 494 | "ext-curl": "*", 495 | "php": ">=5.3.3", 496 | "symfony/event-dispatcher": ">=2.1" 497 | }, 498 | "replace": { 499 | "guzzle/batch": "self.version", 500 | "guzzle/cache": "self.version", 501 | "guzzle/common": "self.version", 502 | "guzzle/http": "self.version", 503 | "guzzle/inflection": "self.version", 504 | "guzzle/iterator": "self.version", 505 | "guzzle/log": "self.version", 506 | "guzzle/parser": "self.version", 507 | "guzzle/plugin": "self.version", 508 | "guzzle/plugin-async": "self.version", 509 | "guzzle/plugin-backoff": "self.version", 510 | "guzzle/plugin-cache": "self.version", 511 | "guzzle/plugin-cookie": "self.version", 512 | "guzzle/plugin-curlauth": "self.version", 513 | "guzzle/plugin-error-response": "self.version", 514 | "guzzle/plugin-history": "self.version", 515 | "guzzle/plugin-log": "self.version", 516 | "guzzle/plugin-md5": "self.version", 517 | "guzzle/plugin-mock": "self.version", 518 | "guzzle/plugin-oauth": "self.version", 519 | "guzzle/service": "self.version", 520 | "guzzle/stream": "self.version" 521 | }, 522 | "require-dev": { 523 | "doctrine/cache": "*", 524 | "monolog/monolog": "1.*", 525 | "phpunit/phpunit": "3.7.*", 526 | "psr/log": "1.0.*", 527 | "symfony/class-loader": "*", 528 | "zendframework/zend-cache": "<2.3", 529 | "zendframework/zend-log": "<2.3" 530 | }, 531 | "type": "library", 532 | "extra": { 533 | "branch-alias": { 534 | "dev-master": "3.8-dev" 535 | } 536 | }, 537 | "autoload": { 538 | "psr-0": { 539 | "Guzzle": "src/", 540 | "Guzzle\\Tests": "tests/" 541 | } 542 | }, 543 | "notification-url": "https://packagist.org/downloads/", 544 | "license": [ 545 | "MIT" 546 | ], 547 | "authors": [ 548 | { 549 | "name": "Michael Dowling", 550 | "email": "mtdowling@gmail.com", 551 | "homepage": "https://github.com/mtdowling" 552 | }, 553 | { 554 | "name": "Guzzle Community", 555 | "homepage": "https://github.com/guzzle/guzzle/contributors" 556 | } 557 | ], 558 | "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", 559 | "homepage": "http://guzzlephp.org/", 560 | "keywords": [ 561 | "client", 562 | "curl", 563 | "framework", 564 | "http", 565 | "http client", 566 | "rest", 567 | "web service" 568 | ], 569 | "time": "2014-01-28T22:29:15+00:00" 570 | }, 571 | { 572 | "name": "lusitanian/oauth", 573 | "version": "v0.2.5", 574 | "source": { 575 | "type": "git", 576 | "url": "https://github.com/Lusitanian/PHPoAuthLib.git", 577 | "reference": "27e375e13e1badcd6dca7fb47b154b3c48fdec0c" 578 | }, 579 | "dist": { 580 | "type": "zip", 581 | "url": "https://api.github.com/repos/Lusitanian/PHPoAuthLib/zipball/27e375e13e1badcd6dca7fb47b154b3c48fdec0c", 582 | "reference": "27e375e13e1badcd6dca7fb47b154b3c48fdec0c", 583 | "shasum": "" 584 | }, 585 | "require": { 586 | "php": ">=5.3.0" 587 | }, 588 | "require-dev": { 589 | "phpunit/phpunit": "3.7.*", 590 | "predis/predis": "0.8.*@dev", 591 | "symfony/http-foundation": "~2.1" 592 | }, 593 | "suggest": { 594 | "predis/predis": "Allows using the Redis storage backend.", 595 | "symfony/http-foundation": "Allows using the Symfony Session storage backend." 596 | }, 597 | "type": "library", 598 | "extra": { 599 | "branch-alias": { 600 | "dev-master": "0.1-dev" 601 | } 602 | }, 603 | "autoload": { 604 | "psr-0": { 605 | "OAuth": "src", 606 | "OAuth\\Unit": "tests" 607 | } 608 | }, 609 | "notification-url": "https://packagist.org/downloads/", 610 | "license": [ 611 | "MIT" 612 | ], 613 | "authors": [ 614 | { 615 | "name": "David Desberg", 616 | "email": "david@daviddesberg.com" 617 | }, 618 | { 619 | "name": "Pieter Hordijk", 620 | "email": "info@pieterhordijk.com", 621 | "homepage": "https://pieterhordijk.com", 622 | "role": "Developer" 623 | } 624 | ], 625 | "description": "PHP 5.3+ oAuth 1/2 Library", 626 | "keywords": [ 627 | "Authentication", 628 | "authorization", 629 | "oauth", 630 | "security" 631 | ], 632 | "time": "2013-12-25T20:05:42+00:00" 633 | }, 634 | { 635 | "name": "monolog/monolog", 636 | "version": "1.23.0", 637 | "source": { 638 | "type": "git", 639 | "url": "https://github.com/Seldaek/monolog.git", 640 | "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4" 641 | }, 642 | "dist": { 643 | "type": "zip", 644 | "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4", 645 | "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4", 646 | "shasum": "" 647 | }, 648 | "require": { 649 | "php": ">=5.3.0", 650 | "psr/log": "~1.0" 651 | }, 652 | "provide": { 653 | "psr/log-implementation": "1.0.0" 654 | }, 655 | "require-dev": { 656 | "aws/aws-sdk-php": "^2.4.9 || ^3.0", 657 | "doctrine/couchdb": "~1.0@dev", 658 | "graylog2/gelf-php": "~1.0", 659 | "jakub-onderka/php-parallel-lint": "0.9", 660 | "php-amqplib/php-amqplib": "~2.4", 661 | "php-console/php-console": "^3.1.3", 662 | "phpunit/phpunit": "~4.5", 663 | "phpunit/phpunit-mock-objects": "2.3.0", 664 | "ruflin/elastica": ">=0.90 <3.0", 665 | "sentry/sentry": "^0.13", 666 | "swiftmailer/swiftmailer": "^5.3|^6.0" 667 | }, 668 | "suggest": { 669 | "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", 670 | "doctrine/couchdb": "Allow sending log messages to a CouchDB server", 671 | "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", 672 | "ext-mongo": "Allow sending log messages to a MongoDB server", 673 | "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", 674 | "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", 675 | "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", 676 | "php-console/php-console": "Allow sending log messages to Google Chrome", 677 | "rollbar/rollbar": "Allow sending log messages to Rollbar", 678 | "ruflin/elastica": "Allow sending log messages to an Elastic Search server", 679 | "sentry/sentry": "Allow sending log messages to a Sentry server" 680 | }, 681 | "type": "library", 682 | "extra": { 683 | "branch-alias": { 684 | "dev-master": "2.0.x-dev" 685 | } 686 | }, 687 | "autoload": { 688 | "psr-4": { 689 | "Monolog\\": "src/Monolog" 690 | } 691 | }, 692 | "notification-url": "https://packagist.org/downloads/", 693 | "license": [ 694 | "MIT" 695 | ], 696 | "authors": [ 697 | { 698 | "name": "Jordi Boggiano", 699 | "email": "j.boggiano@seld.be", 700 | "homepage": "http://seld.be" 701 | } 702 | ], 703 | "description": "Sends your logs to files, sockets, inboxes, databases and various web services", 704 | "homepage": "http://github.com/Seldaek/monolog", 705 | "keywords": [ 706 | "log", 707 | "logging", 708 | "psr-3" 709 | ], 710 | "time": "2017-06-19T01:22:40+00:00" 711 | }, 712 | { 713 | "name": "psr/log", 714 | "version": "1.0.2", 715 | "source": { 716 | "type": "git", 717 | "url": "https://github.com/php-fig/log.git", 718 | "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" 719 | }, 720 | "dist": { 721 | "type": "zip", 722 | "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", 723 | "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", 724 | "shasum": "" 725 | }, 726 | "require": { 727 | "php": ">=5.3.0" 728 | }, 729 | "type": "library", 730 | "extra": { 731 | "branch-alias": { 732 | "dev-master": "1.0.x-dev" 733 | } 734 | }, 735 | "autoload": { 736 | "psr-4": { 737 | "Psr\\Log\\": "Psr/Log/" 738 | } 739 | }, 740 | "notification-url": "https://packagist.org/downloads/", 741 | "license": [ 742 | "MIT" 743 | ], 744 | "authors": [ 745 | { 746 | "name": "PHP-FIG", 747 | "homepage": "http://www.php-fig.org/" 748 | } 749 | ], 750 | "description": "Common interface for logging libraries", 751 | "homepage": "https://github.com/php-fig/log", 752 | "keywords": [ 753 | "log", 754 | "psr", 755 | "psr-3" 756 | ], 757 | "time": "2016-10-10T12:19:37+00:00" 758 | }, 759 | { 760 | "name": "rezzza/flickr", 761 | "version": "v1.1.0", 762 | "source": { 763 | "type": "git", 764 | "url": "https://github.com/rezzza/flickr.git", 765 | "reference": "97bf6ce57614ac793a6aaecf887a0da1dcc9c281" 766 | }, 767 | "dist": { 768 | "type": "zip", 769 | "url": "https://api.github.com/repos/rezzza/flickr/zipball/97bf6ce57614ac793a6aaecf887a0da1dcc9c281", 770 | "reference": "97bf6ce57614ac793a6aaecf887a0da1dcc9c281", 771 | "shasum": "" 772 | }, 773 | "require": { 774 | "php": ">=5.3.2" 775 | }, 776 | "require-dev": { 777 | "guzzle/http": "3.*", 778 | "phpunit/phpunit": "3.7.32" 779 | }, 780 | "suggest": { 781 | "guzzle/guzzle": "HTTP wrapper", 782 | "guzzle/http": "Guzzle Adapter for http requests" 783 | }, 784 | "type": "standalone", 785 | "extra": { 786 | "branch-alias": { 787 | "dev-master": "1.0.x-dev" 788 | } 789 | }, 790 | "autoload": { 791 | "psr-0": { 792 | "Rezzza\\Flickr": "src/" 793 | } 794 | }, 795 | "notification-url": "https://packagist.org/downloads/", 796 | "license": [ 797 | "MIT" 798 | ], 799 | "authors": [ 800 | { 801 | "name": "Community contributors", 802 | "homepage": "https://github.com/rezzza/flickr/contributors" 803 | } 804 | ], 805 | "description": "Flickr API Wrapper", 806 | "homepage": "https://github.com/rezzza/flickr", 807 | "keywords": [ 808 | "flickr" 809 | ], 810 | "time": "2014-02-26T09:18:38+00:00" 811 | }, 812 | { 813 | "name": "rych/bytesize", 814 | "version": "v1.0.0", 815 | "source": { 816 | "type": "git", 817 | "url": "https://github.com/rchouinard/bytesize.git", 818 | "reference": "297e16ea047461b91e8d7eb90aa46aaa52917824" 819 | }, 820 | "dist": { 821 | "type": "zip", 822 | "url": "https://api.github.com/repos/rchouinard/bytesize/zipball/297e16ea047461b91e8d7eb90aa46aaa52917824", 823 | "reference": "297e16ea047461b91e8d7eb90aa46aaa52917824", 824 | "shasum": "" 825 | }, 826 | "require": { 827 | "ext-bcmath": "*", 828 | "php": ">=5.3.4" 829 | }, 830 | "require-dev": { 831 | "phpunit/phpunit": "3.7.*" 832 | }, 833 | "type": "library", 834 | "autoload": { 835 | "psr-4": { 836 | "Rych\\ByteSize\\": "src/" 837 | } 838 | }, 839 | "notification-url": "https://packagist.org/downloads/", 840 | "license": [ 841 | "MIT" 842 | ], 843 | "authors": [ 844 | { 845 | "name": "Ryan Chouinard", 846 | "email": "rchouinard@gmail.com", 847 | "homepage": "http://ryanchouinard.com" 848 | } 849 | ], 850 | "description": "Utility component for nicely formatted file sizes.", 851 | "homepage": "https://github.com/rchouinard/bytesize", 852 | "keywords": [ 853 | "filesize" 854 | ], 855 | "time": "2014-04-04T18:06:18+00:00" 856 | }, 857 | { 858 | "name": "symfony/console", 859 | "version": "v3.3.13", 860 | "source": { 861 | "type": "git", 862 | "url": "https://github.com/symfony/console.git", 863 | "reference": "63cd7960a0a522c3537f6326706d7f3b8de65805" 864 | }, 865 | "dist": { 866 | "type": "zip", 867 | "url": "https://api.github.com/repos/symfony/console/zipball/63cd7960a0a522c3537f6326706d7f3b8de65805", 868 | "reference": "63cd7960a0a522c3537f6326706d7f3b8de65805", 869 | "shasum": "" 870 | }, 871 | "require": { 872 | "php": "^5.5.9|>=7.0.8", 873 | "symfony/debug": "~2.8|~3.0", 874 | "symfony/polyfill-mbstring": "~1.0" 875 | }, 876 | "conflict": { 877 | "symfony/dependency-injection": "<3.3" 878 | }, 879 | "require-dev": { 880 | "psr/log": "~1.0", 881 | "symfony/config": "~3.3", 882 | "symfony/dependency-injection": "~3.3", 883 | "symfony/event-dispatcher": "~2.8|~3.0", 884 | "symfony/filesystem": "~2.8|~3.0", 885 | "symfony/process": "~2.8|~3.0" 886 | }, 887 | "suggest": { 888 | "psr/log": "For using the console logger", 889 | "symfony/event-dispatcher": "", 890 | "symfony/filesystem": "", 891 | "symfony/process": "" 892 | }, 893 | "type": "library", 894 | "extra": { 895 | "branch-alias": { 896 | "dev-master": "3.3-dev" 897 | } 898 | }, 899 | "autoload": { 900 | "psr-4": { 901 | "Symfony\\Component\\Console\\": "" 902 | }, 903 | "exclude-from-classmap": [ 904 | "/Tests/" 905 | ] 906 | }, 907 | "notification-url": "https://packagist.org/downloads/", 908 | "license": [ 909 | "MIT" 910 | ], 911 | "authors": [ 912 | { 913 | "name": "Fabien Potencier", 914 | "email": "fabien@symfony.com" 915 | }, 916 | { 917 | "name": "Symfony Community", 918 | "homepage": "https://symfony.com/contributors" 919 | } 920 | ], 921 | "description": "Symfony Console Component", 922 | "homepage": "https://symfony.com", 923 | "time": "2017-11-16T15:24:32+00:00" 924 | }, 925 | { 926 | "name": "symfony/debug", 927 | "version": "v3.3.13", 928 | "source": { 929 | "type": "git", 930 | "url": "https://github.com/symfony/debug.git", 931 | "reference": "74557880e2846b5c84029faa96b834da37e29810" 932 | }, 933 | "dist": { 934 | "type": "zip", 935 | "url": "https://api.github.com/repos/symfony/debug/zipball/74557880e2846b5c84029faa96b834da37e29810", 936 | "reference": "74557880e2846b5c84029faa96b834da37e29810", 937 | "shasum": "" 938 | }, 939 | "require": { 940 | "php": "^5.5.9|>=7.0.8", 941 | "psr/log": "~1.0" 942 | }, 943 | "conflict": { 944 | "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" 945 | }, 946 | "require-dev": { 947 | "symfony/http-kernel": "~2.8|~3.0" 948 | }, 949 | "type": "library", 950 | "extra": { 951 | "branch-alias": { 952 | "dev-master": "3.3-dev" 953 | } 954 | }, 955 | "autoload": { 956 | "psr-4": { 957 | "Symfony\\Component\\Debug\\": "" 958 | }, 959 | "exclude-from-classmap": [ 960 | "/Tests/" 961 | ] 962 | }, 963 | "notification-url": "https://packagist.org/downloads/", 964 | "license": [ 965 | "MIT" 966 | ], 967 | "authors": [ 968 | { 969 | "name": "Fabien Potencier", 970 | "email": "fabien@symfony.com" 971 | }, 972 | { 973 | "name": "Symfony Community", 974 | "homepage": "https://symfony.com/contributors" 975 | } 976 | ], 977 | "description": "Symfony Debug Component", 978 | "homepage": "https://symfony.com", 979 | "time": "2017-11-10T16:38:39+00:00" 980 | }, 981 | { 982 | "name": "symfony/event-dispatcher", 983 | "version": "v3.3.13", 984 | "source": { 985 | "type": "git", 986 | "url": "https://github.com/symfony/event-dispatcher.git", 987 | "reference": "271d8c27c3ec5ecee6e2ac06016232e249d638d9" 988 | }, 989 | "dist": { 990 | "type": "zip", 991 | "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/271d8c27c3ec5ecee6e2ac06016232e249d638d9", 992 | "reference": "271d8c27c3ec5ecee6e2ac06016232e249d638d9", 993 | "shasum": "" 994 | }, 995 | "require": { 996 | "php": "^5.5.9|>=7.0.8" 997 | }, 998 | "conflict": { 999 | "symfony/dependency-injection": "<3.3" 1000 | }, 1001 | "require-dev": { 1002 | "psr/log": "~1.0", 1003 | "symfony/config": "~2.8|~3.0", 1004 | "symfony/dependency-injection": "~3.3", 1005 | "symfony/expression-language": "~2.8|~3.0", 1006 | "symfony/stopwatch": "~2.8|~3.0" 1007 | }, 1008 | "suggest": { 1009 | "symfony/dependency-injection": "", 1010 | "symfony/http-kernel": "" 1011 | }, 1012 | "type": "library", 1013 | "extra": { 1014 | "branch-alias": { 1015 | "dev-master": "3.3-dev" 1016 | } 1017 | }, 1018 | "autoload": { 1019 | "psr-4": { 1020 | "Symfony\\Component\\EventDispatcher\\": "" 1021 | }, 1022 | "exclude-from-classmap": [ 1023 | "/Tests/" 1024 | ] 1025 | }, 1026 | "notification-url": "https://packagist.org/downloads/", 1027 | "license": [ 1028 | "MIT" 1029 | ], 1030 | "authors": [ 1031 | { 1032 | "name": "Fabien Potencier", 1033 | "email": "fabien@symfony.com" 1034 | }, 1035 | { 1036 | "name": "Symfony Community", 1037 | "homepage": "https://symfony.com/contributors" 1038 | } 1039 | ], 1040 | "description": "Symfony EventDispatcher Component", 1041 | "homepage": "https://symfony.com", 1042 | "time": "2017-11-05T15:47:03+00:00" 1043 | }, 1044 | { 1045 | "name": "symfony/filesystem", 1046 | "version": "v3.3.13", 1047 | "source": { 1048 | "type": "git", 1049 | "url": "https://github.com/symfony/filesystem.git", 1050 | "reference": "77db266766b54db3ee982fe51868328b887ce15c" 1051 | }, 1052 | "dist": { 1053 | "type": "zip", 1054 | "url": "https://api.github.com/repos/symfony/filesystem/zipball/77db266766b54db3ee982fe51868328b887ce15c", 1055 | "reference": "77db266766b54db3ee982fe51868328b887ce15c", 1056 | "shasum": "" 1057 | }, 1058 | "require": { 1059 | "php": "^5.5.9|>=7.0.8" 1060 | }, 1061 | "type": "library", 1062 | "extra": { 1063 | "branch-alias": { 1064 | "dev-master": "3.3-dev" 1065 | } 1066 | }, 1067 | "autoload": { 1068 | "psr-4": { 1069 | "Symfony\\Component\\Filesystem\\": "" 1070 | }, 1071 | "exclude-from-classmap": [ 1072 | "/Tests/" 1073 | ] 1074 | }, 1075 | "notification-url": "https://packagist.org/downloads/", 1076 | "license": [ 1077 | "MIT" 1078 | ], 1079 | "authors": [ 1080 | { 1081 | "name": "Fabien Potencier", 1082 | "email": "fabien@symfony.com" 1083 | }, 1084 | { 1085 | "name": "Symfony Community", 1086 | "homepage": "https://symfony.com/contributors" 1087 | } 1088 | ], 1089 | "description": "Symfony Filesystem Component", 1090 | "homepage": "https://symfony.com", 1091 | "time": "2017-11-07T14:12:55+00:00" 1092 | }, 1093 | { 1094 | "name": "symfony/finder", 1095 | "version": "v3.3.13", 1096 | "source": { 1097 | "type": "git", 1098 | "url": "https://github.com/symfony/finder.git", 1099 | "reference": "138af5ec075d4b1d1bd19de08c38a34bb2d7d880" 1100 | }, 1101 | "dist": { 1102 | "type": "zip", 1103 | "url": "https://api.github.com/repos/symfony/finder/zipball/138af5ec075d4b1d1bd19de08c38a34bb2d7d880", 1104 | "reference": "138af5ec075d4b1d1bd19de08c38a34bb2d7d880", 1105 | "shasum": "" 1106 | }, 1107 | "require": { 1108 | "php": "^5.5.9|>=7.0.8" 1109 | }, 1110 | "type": "library", 1111 | "extra": { 1112 | "branch-alias": { 1113 | "dev-master": "3.3-dev" 1114 | } 1115 | }, 1116 | "autoload": { 1117 | "psr-4": { 1118 | "Symfony\\Component\\Finder\\": "" 1119 | }, 1120 | "exclude-from-classmap": [ 1121 | "/Tests/" 1122 | ] 1123 | }, 1124 | "notification-url": "https://packagist.org/downloads/", 1125 | "license": [ 1126 | "MIT" 1127 | ], 1128 | "authors": [ 1129 | { 1130 | "name": "Fabien Potencier", 1131 | "email": "fabien@symfony.com" 1132 | }, 1133 | { 1134 | "name": "Symfony Community", 1135 | "homepage": "https://symfony.com/contributors" 1136 | } 1137 | ], 1138 | "description": "Symfony Finder Component", 1139 | "homepage": "https://symfony.com", 1140 | "time": "2017-11-05T15:47:03+00:00" 1141 | }, 1142 | { 1143 | "name": "symfony/polyfill-mbstring", 1144 | "version": "v1.6.0", 1145 | "source": { 1146 | "type": "git", 1147 | "url": "https://github.com/symfony/polyfill-mbstring.git", 1148 | "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296" 1149 | }, 1150 | "dist": { 1151 | "type": "zip", 1152 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296", 1153 | "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296", 1154 | "shasum": "" 1155 | }, 1156 | "require": { 1157 | "php": ">=5.3.3" 1158 | }, 1159 | "suggest": { 1160 | "ext-mbstring": "For best performance" 1161 | }, 1162 | "type": "library", 1163 | "extra": { 1164 | "branch-alias": { 1165 | "dev-master": "1.6-dev" 1166 | } 1167 | }, 1168 | "autoload": { 1169 | "psr-4": { 1170 | "Symfony\\Polyfill\\Mbstring\\": "" 1171 | }, 1172 | "files": [ 1173 | "bootstrap.php" 1174 | ] 1175 | }, 1176 | "notification-url": "https://packagist.org/downloads/", 1177 | "license": [ 1178 | "MIT" 1179 | ], 1180 | "authors": [ 1181 | { 1182 | "name": "Nicolas Grekas", 1183 | "email": "p@tchwork.com" 1184 | }, 1185 | { 1186 | "name": "Symfony Community", 1187 | "homepage": "https://symfony.com/contributors" 1188 | } 1189 | ], 1190 | "description": "Symfony polyfill for the Mbstring extension", 1191 | "homepage": "https://symfony.com", 1192 | "keywords": [ 1193 | "compatibility", 1194 | "mbstring", 1195 | "polyfill", 1196 | "portable", 1197 | "shim" 1198 | ], 1199 | "time": "2017-10-11T12:05:26+00:00" 1200 | }, 1201 | { 1202 | "name": "symfony/yaml", 1203 | "version": "v2.8.31", 1204 | "source": { 1205 | "type": "git", 1206 | "url": "https://github.com/symfony/yaml.git", 1207 | "reference": "d819bf267e901727141fe828ae888486fd21236e" 1208 | }, 1209 | "dist": { 1210 | "type": "zip", 1211 | "url": "https://api.github.com/repos/symfony/yaml/zipball/d819bf267e901727141fe828ae888486fd21236e", 1212 | "reference": "d819bf267e901727141fe828ae888486fd21236e", 1213 | "shasum": "" 1214 | }, 1215 | "require": { 1216 | "php": ">=5.3.9" 1217 | }, 1218 | "type": "library", 1219 | "extra": { 1220 | "branch-alias": { 1221 | "dev-master": "2.8-dev" 1222 | } 1223 | }, 1224 | "autoload": { 1225 | "psr-4": { 1226 | "Symfony\\Component\\Yaml\\": "" 1227 | }, 1228 | "exclude-from-classmap": [ 1229 | "/Tests/" 1230 | ] 1231 | }, 1232 | "notification-url": "https://packagist.org/downloads/", 1233 | "license": [ 1234 | "MIT" 1235 | ], 1236 | "authors": [ 1237 | { 1238 | "name": "Fabien Potencier", 1239 | "email": "fabien@symfony.com" 1240 | }, 1241 | { 1242 | "name": "Symfony Community", 1243 | "homepage": "https://symfony.com/contributors" 1244 | } 1245 | ], 1246 | "description": "Symfony Yaml Component", 1247 | "homepage": "https://symfony.com", 1248 | "time": "2017-11-05T15:25:56+00:00" 1249 | } 1250 | ], 1251 | "packages-dev": [ 1252 | { 1253 | "name": "nette/bootstrap", 1254 | "version": "v2.4.5", 1255 | "source": { 1256 | "type": "git", 1257 | "url": "https://github.com/nette/bootstrap.git", 1258 | "reference": "804925787764d708a7782ea0d9382a310bb21968" 1259 | }, 1260 | "dist": { 1261 | "type": "zip", 1262 | "url": "https://api.github.com/repos/nette/bootstrap/zipball/804925787764d708a7782ea0d9382a310bb21968", 1263 | "reference": "804925787764d708a7782ea0d9382a310bb21968", 1264 | "shasum": "" 1265 | }, 1266 | "require": { 1267 | "nette/di": "~2.4.7", 1268 | "nette/utils": "~2.4", 1269 | "php": ">=5.6.0" 1270 | }, 1271 | "conflict": { 1272 | "nette/nette": "<2.2" 1273 | }, 1274 | "require-dev": { 1275 | "latte/latte": "~2.2", 1276 | "nette/application": "~2.3", 1277 | "nette/caching": "~2.3", 1278 | "nette/database": "~2.3", 1279 | "nette/forms": "~2.3", 1280 | "nette/http": "~2.4.0", 1281 | "nette/mail": "~2.3", 1282 | "nette/robot-loader": "^2.4.2 || ^3.0", 1283 | "nette/safe-stream": "~2.2", 1284 | "nette/security": "~2.3", 1285 | "nette/tester": "~2.0", 1286 | "tracy/tracy": "^2.4.1" 1287 | }, 1288 | "suggest": { 1289 | "nette/robot-loader": "to use Configurator::createRobotLoader()", 1290 | "tracy/tracy": "to use Configurator::enableTracy()" 1291 | }, 1292 | "type": "library", 1293 | "extra": { 1294 | "branch-alias": { 1295 | "dev-master": "2.4-dev" 1296 | } 1297 | }, 1298 | "autoload": { 1299 | "classmap": [ 1300 | "src/" 1301 | ] 1302 | }, 1303 | "notification-url": "https://packagist.org/downloads/", 1304 | "license": [ 1305 | "BSD-3-Clause", 1306 | "GPL-2.0", 1307 | "GPL-3.0" 1308 | ], 1309 | "authors": [ 1310 | { 1311 | "name": "David Grudl", 1312 | "homepage": "https://davidgrudl.com" 1313 | }, 1314 | { 1315 | "name": "Nette Community", 1316 | "homepage": "https://nette.org/contributors" 1317 | } 1318 | ], 1319 | "description": "🅱 Nette Bootstrap: the simple way to configure and bootstrap your Nette application.", 1320 | "homepage": "https://nette.org", 1321 | "keywords": [ 1322 | "bootstrapping", 1323 | "configurator", 1324 | "nette" 1325 | ], 1326 | "time": "2017-08-20T17:36:59+00:00" 1327 | }, 1328 | { 1329 | "name": "nette/caching", 1330 | "version": "v2.5.6", 1331 | "source": { 1332 | "type": "git", 1333 | "url": "https://github.com/nette/caching.git", 1334 | "reference": "1231735b5135ca02bd381b70482c052d2a90bdc9" 1335 | }, 1336 | "dist": { 1337 | "type": "zip", 1338 | "url": "https://api.github.com/repos/nette/caching/zipball/1231735b5135ca02bd381b70482c052d2a90bdc9", 1339 | "reference": "1231735b5135ca02bd381b70482c052d2a90bdc9", 1340 | "shasum": "" 1341 | }, 1342 | "require": { 1343 | "nette/finder": "^2.2 || ~3.0.0", 1344 | "nette/utils": "^2.4 || ~3.0.0", 1345 | "php": ">=5.6.0" 1346 | }, 1347 | "conflict": { 1348 | "nette/nette": "<2.2" 1349 | }, 1350 | "require-dev": { 1351 | "latte/latte": "^2.4", 1352 | "nette/di": "^2.4 || ~3.0.0", 1353 | "nette/tester": "^2.0", 1354 | "tracy/tracy": "^2.4" 1355 | }, 1356 | "suggest": { 1357 | "ext-pdo_sqlite": "to use SQLiteStorage or SQLiteJournal" 1358 | }, 1359 | "type": "library", 1360 | "extra": { 1361 | "branch-alias": { 1362 | "dev-master": "2.5-dev" 1363 | } 1364 | }, 1365 | "autoload": { 1366 | "classmap": [ 1367 | "src/" 1368 | ] 1369 | }, 1370 | "notification-url": "https://packagist.org/downloads/", 1371 | "license": [ 1372 | "BSD-3-Clause", 1373 | "GPL-2.0", 1374 | "GPL-3.0" 1375 | ], 1376 | "authors": [ 1377 | { 1378 | "name": "David Grudl", 1379 | "homepage": "https://davidgrudl.com" 1380 | }, 1381 | { 1382 | "name": "Nette Community", 1383 | "homepage": "https://nette.org/contributors" 1384 | } 1385 | ], 1386 | "description": "⏱ Nette Caching: library with easy-to-use API and many cache backends.", 1387 | "homepage": "https://nette.org", 1388 | "keywords": [ 1389 | "cache", 1390 | "journal", 1391 | "memcached", 1392 | "nette", 1393 | "sqlite" 1394 | ], 1395 | "time": "2017-08-30T12:12:25+00:00" 1396 | }, 1397 | { 1398 | "name": "nette/di", 1399 | "version": "v2.4.10", 1400 | "source": { 1401 | "type": "git", 1402 | "url": "https://github.com/nette/di.git", 1403 | "reference": "a4b3be935b755f23aebea1ce33d7e3c832cdff98" 1404 | }, 1405 | "dist": { 1406 | "type": "zip", 1407 | "url": "https://api.github.com/repos/nette/di/zipball/a4b3be935b755f23aebea1ce33d7e3c832cdff98", 1408 | "reference": "a4b3be935b755f23aebea1ce33d7e3c832cdff98", 1409 | "shasum": "" 1410 | }, 1411 | "require": { 1412 | "ext-tokenizer": "*", 1413 | "nette/neon": "^2.3.3 || ~3.0.0", 1414 | "nette/php-generator": "^2.6.1 || ~3.0.0", 1415 | "nette/utils": "^2.4.3 || ~3.0.0", 1416 | "php": ">=5.6.0" 1417 | }, 1418 | "conflict": { 1419 | "nette/bootstrap": "<2.4", 1420 | "nette/nette": "<2.2" 1421 | }, 1422 | "require-dev": { 1423 | "nette/tester": "^2.0", 1424 | "tracy/tracy": "^2.3" 1425 | }, 1426 | "type": "library", 1427 | "extra": { 1428 | "branch-alias": { 1429 | "dev-master": "2.4-dev" 1430 | } 1431 | }, 1432 | "autoload": { 1433 | "classmap": [ 1434 | "src/" 1435 | ] 1436 | }, 1437 | "notification-url": "https://packagist.org/downloads/", 1438 | "license": [ 1439 | "BSD-3-Clause", 1440 | "GPL-2.0", 1441 | "GPL-3.0" 1442 | ], 1443 | "authors": [ 1444 | { 1445 | "name": "David Grudl", 1446 | "homepage": "https://davidgrudl.com" 1447 | }, 1448 | { 1449 | "name": "Nette Community", 1450 | "homepage": "https://nette.org/contributors" 1451 | } 1452 | ], 1453 | "description": "💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP 7.1 features.", 1454 | "homepage": "https://nette.org", 1455 | "keywords": [ 1456 | "compiled", 1457 | "di", 1458 | "dic", 1459 | "factory", 1460 | "ioc", 1461 | "nette", 1462 | "static" 1463 | ], 1464 | "time": "2017-08-31T22:42:00+00:00" 1465 | }, 1466 | { 1467 | "name": "nette/finder", 1468 | "version": "v2.4.1", 1469 | "source": { 1470 | "type": "git", 1471 | "url": "https://github.com/nette/finder.git", 1472 | "reference": "4d43a66d072c57d585bf08a3ef68d3587f7e9547" 1473 | }, 1474 | "dist": { 1475 | "type": "zip", 1476 | "url": "https://api.github.com/repos/nette/finder/zipball/4d43a66d072c57d585bf08a3ef68d3587f7e9547", 1477 | "reference": "4d43a66d072c57d585bf08a3ef68d3587f7e9547", 1478 | "shasum": "" 1479 | }, 1480 | "require": { 1481 | "nette/utils": "^2.4 || ~3.0.0", 1482 | "php": ">=5.6.0" 1483 | }, 1484 | "conflict": { 1485 | "nette/nette": "<2.2" 1486 | }, 1487 | "require-dev": { 1488 | "nette/tester": "^2.0", 1489 | "tracy/tracy": "^2.3" 1490 | }, 1491 | "type": "library", 1492 | "extra": { 1493 | "branch-alias": { 1494 | "dev-master": "2.4-dev" 1495 | } 1496 | }, 1497 | "autoload": { 1498 | "classmap": [ 1499 | "src/" 1500 | ] 1501 | }, 1502 | "notification-url": "https://packagist.org/downloads/", 1503 | "license": [ 1504 | "BSD-3-Clause", 1505 | "GPL-2.0", 1506 | "GPL-3.0" 1507 | ], 1508 | "authors": [ 1509 | { 1510 | "name": "David Grudl", 1511 | "homepage": "https://davidgrudl.com" 1512 | }, 1513 | { 1514 | "name": "Nette Community", 1515 | "homepage": "https://nette.org/contributors" 1516 | } 1517 | ], 1518 | "description": "Nette Finder: Files Searching", 1519 | "homepage": "https://nette.org", 1520 | "time": "2017-07-10T23:47:08+00:00" 1521 | }, 1522 | { 1523 | "name": "nette/neon", 1524 | "version": "v2.4.2", 1525 | "source": { 1526 | "type": "git", 1527 | "url": "https://github.com/nette/neon.git", 1528 | "reference": "9eacd50553b26b53a3977bfb2fea2166d4331622" 1529 | }, 1530 | "dist": { 1531 | "type": "zip", 1532 | "url": "https://api.github.com/repos/nette/neon/zipball/9eacd50553b26b53a3977bfb2fea2166d4331622", 1533 | "reference": "9eacd50553b26b53a3977bfb2fea2166d4331622", 1534 | "shasum": "" 1535 | }, 1536 | "require": { 1537 | "ext-iconv": "*", 1538 | "ext-json": "*", 1539 | "php": ">=5.6.0" 1540 | }, 1541 | "require-dev": { 1542 | "nette/tester": "~2.0", 1543 | "tracy/tracy": "^2.3" 1544 | }, 1545 | "type": "library", 1546 | "extra": { 1547 | "branch-alias": { 1548 | "dev-master": "2.4-dev" 1549 | } 1550 | }, 1551 | "autoload": { 1552 | "classmap": [ 1553 | "src/" 1554 | ] 1555 | }, 1556 | "notification-url": "https://packagist.org/downloads/", 1557 | "license": [ 1558 | "BSD-3-Clause", 1559 | "GPL-2.0", 1560 | "GPL-3.0" 1561 | ], 1562 | "authors": [ 1563 | { 1564 | "name": "David Grudl", 1565 | "homepage": "https://davidgrudl.com" 1566 | }, 1567 | { 1568 | "name": "Nette Community", 1569 | "homepage": "https://nette.org/contributors" 1570 | } 1571 | ], 1572 | "description": "Nette NEON: parser & generator for Nette Object Notation", 1573 | "homepage": "http://ne-on.org", 1574 | "time": "2017-07-11T18:29:08+00:00" 1575 | }, 1576 | { 1577 | "name": "nette/php-generator", 1578 | "version": "v3.0.1", 1579 | "source": { 1580 | "type": "git", 1581 | "url": "https://github.com/nette/php-generator.git", 1582 | "reference": "eb2dbc9c3409e9db40568109ca4994d51373b60c" 1583 | }, 1584 | "dist": { 1585 | "type": "zip", 1586 | "url": "https://api.github.com/repos/nette/php-generator/zipball/eb2dbc9c3409e9db40568109ca4994d51373b60c", 1587 | "reference": "eb2dbc9c3409e9db40568109ca4994d51373b60c", 1588 | "shasum": "" 1589 | }, 1590 | "require": { 1591 | "nette/utils": "^2.4.2 || ~3.0.0", 1592 | "php": ">=7.0" 1593 | }, 1594 | "conflict": { 1595 | "nette/nette": "<2.2" 1596 | }, 1597 | "require-dev": { 1598 | "nette/tester": "^2.0", 1599 | "tracy/tracy": "^2.3" 1600 | }, 1601 | "type": "library", 1602 | "extra": { 1603 | "branch-alias": { 1604 | "dev-master": "3.0-dev" 1605 | } 1606 | }, 1607 | "autoload": { 1608 | "classmap": [ 1609 | "src/" 1610 | ] 1611 | }, 1612 | "notification-url": "https://packagist.org/downloads/", 1613 | "license": [ 1614 | "BSD-3-Clause", 1615 | "GPL-2.0", 1616 | "GPL-3.0" 1617 | ], 1618 | "authors": [ 1619 | { 1620 | "name": "David Grudl", 1621 | "homepage": "https://davidgrudl.com" 1622 | }, 1623 | { 1624 | "name": "Nette Community", 1625 | "homepage": "https://nette.org/contributors" 1626 | } 1627 | ], 1628 | "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.1 features.", 1629 | "homepage": "https://nette.org", 1630 | "keywords": [ 1631 | "code", 1632 | "nette", 1633 | "php", 1634 | "scaffolding" 1635 | ], 1636 | "time": "2017-07-11T19:07:13+00:00" 1637 | }, 1638 | { 1639 | "name": "nette/robot-loader", 1640 | "version": "v3.0.2", 1641 | "source": { 1642 | "type": "git", 1643 | "url": "https://github.com/nette/robot-loader.git", 1644 | "reference": "b703b4f5955831b0bcaacbd2f6af76021b056826" 1645 | }, 1646 | "dist": { 1647 | "type": "zip", 1648 | "url": "https://api.github.com/repos/nette/robot-loader/zipball/b703b4f5955831b0bcaacbd2f6af76021b056826", 1649 | "reference": "b703b4f5955831b0bcaacbd2f6af76021b056826", 1650 | "shasum": "" 1651 | }, 1652 | "require": { 1653 | "ext-tokenizer": "*", 1654 | "nette/finder": "^2.3 || ^3.0", 1655 | "nette/utils": "^2.4 || ^3.0", 1656 | "php": ">=5.6.0" 1657 | }, 1658 | "conflict": { 1659 | "nette/nette": "<2.2" 1660 | }, 1661 | "require-dev": { 1662 | "nette/tester": "^2.0", 1663 | "tracy/tracy": "^2.3" 1664 | }, 1665 | "type": "library", 1666 | "extra": { 1667 | "branch-alias": { 1668 | "dev-master": "3.0-dev" 1669 | } 1670 | }, 1671 | "autoload": { 1672 | "classmap": [ 1673 | "src/" 1674 | ] 1675 | }, 1676 | "notification-url": "https://packagist.org/downloads/", 1677 | "license": [ 1678 | "BSD-3-Clause", 1679 | "GPL-2.0", 1680 | "GPL-3.0" 1681 | ], 1682 | "authors": [ 1683 | { 1684 | "name": "David Grudl", 1685 | "homepage": "https://davidgrudl.com" 1686 | }, 1687 | { 1688 | "name": "Nette Community", 1689 | "homepage": "https://nette.org/contributors" 1690 | } 1691 | ], 1692 | "description": "🍀 Nette RobotLoader: high performance and comfortable autoloader that will search and autoload classes within your application.", 1693 | "homepage": "https://nette.org", 1694 | "keywords": [ 1695 | "autoload", 1696 | "class", 1697 | "interface", 1698 | "nette", 1699 | "trait" 1700 | ], 1701 | "time": "2017-07-18T00:09:56+00:00" 1702 | }, 1703 | { 1704 | "name": "nette/utils", 1705 | "version": "v2.4.8", 1706 | "source": { 1707 | "type": "git", 1708 | "url": "https://github.com/nette/utils.git", 1709 | "reference": "f1584033b5af945b470533b466b81a789d532034" 1710 | }, 1711 | "dist": { 1712 | "type": "zip", 1713 | "url": "https://api.github.com/repos/nette/utils/zipball/f1584033b5af945b470533b466b81a789d532034", 1714 | "reference": "f1584033b5af945b470533b466b81a789d532034", 1715 | "shasum": "" 1716 | }, 1717 | "require": { 1718 | "php": ">=5.6.0" 1719 | }, 1720 | "conflict": { 1721 | "nette/nette": "<2.2" 1722 | }, 1723 | "require-dev": { 1724 | "nette/tester": "~2.0", 1725 | "tracy/tracy": "^2.3" 1726 | }, 1727 | "suggest": { 1728 | "ext-gd": "to use Image", 1729 | "ext-iconv": "to use Strings::webalize() and toAscii()", 1730 | "ext-intl": "for script transliteration in Strings::webalize() and toAscii()", 1731 | "ext-json": "to use Nette\\Utils\\Json", 1732 | "ext-mbstring": "to use Strings::lower() etc...", 1733 | "ext-xml": "to use Strings::length() etc. when mbstring is not available" 1734 | }, 1735 | "type": "library", 1736 | "extra": { 1737 | "branch-alias": { 1738 | "dev-master": "2.4-dev" 1739 | } 1740 | }, 1741 | "autoload": { 1742 | "classmap": [ 1743 | "src/" 1744 | ] 1745 | }, 1746 | "notification-url": "https://packagist.org/downloads/", 1747 | "license": [ 1748 | "BSD-3-Clause", 1749 | "GPL-2.0", 1750 | "GPL-3.0" 1751 | ], 1752 | "authors": [ 1753 | { 1754 | "name": "David Grudl", 1755 | "homepage": "https://davidgrudl.com" 1756 | }, 1757 | { 1758 | "name": "Nette Community", 1759 | "homepage": "https://nette.org/contributors" 1760 | } 1761 | ], 1762 | "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", 1763 | "homepage": "https://nette.org", 1764 | "keywords": [ 1765 | "array", 1766 | "core", 1767 | "datetime", 1768 | "images", 1769 | "json", 1770 | "nette", 1771 | "paginator", 1772 | "password", 1773 | "slugify", 1774 | "string", 1775 | "unicode", 1776 | "utf-8", 1777 | "utility", 1778 | "validation" 1779 | ], 1780 | "time": "2017-08-20T17:32:29+00:00" 1781 | }, 1782 | { 1783 | "name": "nikic/php-parser", 1784 | "version": "v3.1.2", 1785 | "source": { 1786 | "type": "git", 1787 | "url": "https://github.com/nikic/PHP-Parser.git", 1788 | "reference": "08131e7ff29de6bb9f12275c7d35df71f25f4d89" 1789 | }, 1790 | "dist": { 1791 | "type": "zip", 1792 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/08131e7ff29de6bb9f12275c7d35df71f25f4d89", 1793 | "reference": "08131e7ff29de6bb9f12275c7d35df71f25f4d89", 1794 | "shasum": "" 1795 | }, 1796 | "require": { 1797 | "ext-tokenizer": "*", 1798 | "php": ">=5.5" 1799 | }, 1800 | "require-dev": { 1801 | "phpunit/phpunit": "~4.0|~5.0" 1802 | }, 1803 | "bin": [ 1804 | "bin/php-parse" 1805 | ], 1806 | "type": "library", 1807 | "extra": { 1808 | "branch-alias": { 1809 | "dev-master": "3.0-dev" 1810 | } 1811 | }, 1812 | "autoload": { 1813 | "psr-4": { 1814 | "PhpParser\\": "lib/PhpParser" 1815 | } 1816 | }, 1817 | "notification-url": "https://packagist.org/downloads/", 1818 | "license": [ 1819 | "BSD-3-Clause" 1820 | ], 1821 | "authors": [ 1822 | { 1823 | "name": "Nikita Popov" 1824 | } 1825 | ], 1826 | "description": "A PHP parser written in PHP", 1827 | "keywords": [ 1828 | "parser", 1829 | "php" 1830 | ], 1831 | "time": "2017-11-04T11:48:34+00:00" 1832 | }, 1833 | { 1834 | "name": "phpstan/phpstan", 1835 | "version": "0.7", 1836 | "source": { 1837 | "type": "git", 1838 | "url": "https://github.com/phpstan/phpstan.git", 1839 | "reference": "8da03c084b2c8e4a92d48f6926f6191c2c7783ad" 1840 | }, 1841 | "dist": { 1842 | "type": "zip", 1843 | "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8da03c084b2c8e4a92d48f6926f6191c2c7783ad", 1844 | "reference": "8da03c084b2c8e4a92d48f6926f6191c2c7783ad", 1845 | "shasum": "" 1846 | }, 1847 | "require": { 1848 | "nette/bootstrap": "^2.4 || ^3.0", 1849 | "nette/caching": "^2.4 || ^3.0", 1850 | "nette/di": "^2.4 || ^3.0", 1851 | "nette/robot-loader": "^2.4.2 || ^3.0", 1852 | "nette/utils": "^2.4 || ^3.0", 1853 | "nikic/php-parser": "^2.1 || ^3.0.2", 1854 | "php": "~7.0", 1855 | "symfony/console": "~2.7 || ~3.0", 1856 | "symfony/finder": "~2.7 || ~3.0" 1857 | }, 1858 | "require-dev": { 1859 | "consistence/coding-standard": "~0.13.0", 1860 | "jakub-onderka/php-parallel-lint": "^0.9.2", 1861 | "phing/phing": "^2.16.0", 1862 | "phpunit/phpunit": "^6.0.7", 1863 | "satooshi/php-coveralls": "^1.0", 1864 | "slevomat/coding-standard": "^2.0" 1865 | }, 1866 | "bin": [ 1867 | "bin/phpstan" 1868 | ], 1869 | "type": "library", 1870 | "extra": { 1871 | "branch-alias": { 1872 | "dev-master": "0.7-dev" 1873 | } 1874 | }, 1875 | "autoload": { 1876 | "psr-4": { 1877 | "PHPStan\\": "src/" 1878 | } 1879 | }, 1880 | "notification-url": "https://packagist.org/downloads/", 1881 | "license": [ 1882 | "MIT" 1883 | ], 1884 | "description": "PHPStan - PHP Static Analysis Tool", 1885 | "time": "2017-05-14T20:37:59+00:00" 1886 | }, 1887 | { 1888 | "name": "squizlabs/php_codesniffer", 1889 | "version": "3.1.1", 1890 | "source": { 1891 | "type": "git", 1892 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 1893 | "reference": "d667e245d5dcd4d7bf80f26f2c947d476b66213e" 1894 | }, 1895 | "dist": { 1896 | "type": "zip", 1897 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d667e245d5dcd4d7bf80f26f2c947d476b66213e", 1898 | "reference": "d667e245d5dcd4d7bf80f26f2c947d476b66213e", 1899 | "shasum": "" 1900 | }, 1901 | "require": { 1902 | "ext-simplexml": "*", 1903 | "ext-tokenizer": "*", 1904 | "ext-xmlwriter": "*", 1905 | "php": ">=5.4.0" 1906 | }, 1907 | "require-dev": { 1908 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0" 1909 | }, 1910 | "bin": [ 1911 | "bin/phpcs", 1912 | "bin/phpcbf" 1913 | ], 1914 | "type": "library", 1915 | "extra": { 1916 | "branch-alias": { 1917 | "dev-master": "3.x-dev" 1918 | } 1919 | }, 1920 | "notification-url": "https://packagist.org/downloads/", 1921 | "license": [ 1922 | "BSD-3-Clause" 1923 | ], 1924 | "authors": [ 1925 | { 1926 | "name": "Greg Sherwood", 1927 | "role": "lead" 1928 | } 1929 | ], 1930 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 1931 | "homepage": "http://www.squizlabs.com/php-codesniffer", 1932 | "keywords": [ 1933 | "phpcs", 1934 | "standards" 1935 | ], 1936 | "time": "2017-10-16T22:40:25+00:00" 1937 | } 1938 | ], 1939 | "aliases": [], 1940 | "minimum-stability": "stable", 1941 | "stability-flags": [], 1942 | "prefer-stable": false, 1943 | "prefer-lowest": false, 1944 | "platform": { 1945 | "php": "^7.0" 1946 | }, 1947 | "platform-dev": [] 1948 | } 1949 | -------------------------------------------------------------------------------- /flickr-cli.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders":[ 3 | { 4 | "path": ".", 5 | "name": "FlickrCLI", 6 | "folder_exclude_patterns": [ "vendor" ], 7 | "file_exclude_patterns": [ ] 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bin/flickr-cli 6 | src 7 | 8 | vendor/ 9 | 10 | -------------------------------------------------------------------------------- /src/TheFox/FlickrCli/Command/AlbumsCommand.php: -------------------------------------------------------------------------------- 1 | setName('albums'); 15 | $this->setDescription('List Photosets.'); 16 | } 17 | 18 | /** 19 | * @param InputInterface $input 20 | * @param OutputInterface $output 21 | * @return int 22 | */ 23 | protected function execute(InputInterface $input, OutputInterface $output): int 24 | { 25 | parent::execute($input, $output); 26 | 27 | $apiService = $this->getApiService(); 28 | 29 | $photosetTitles = $apiService->getPhotosetTitles(); 30 | foreach ($photosetTitles as $photosetId => $photosetTitle) { 31 | pcntl_signal_dispatch(); 32 | if ($this->getExit()) { 33 | break; 34 | } 35 | 36 | printf('%s' . "\n", $photosetTitle); 37 | } 38 | 39 | return $this->getExit(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/TheFox/FlickrCli/Command/AuthCommand.php: -------------------------------------------------------------------------------- 1 | io = new SymfonyStyle($nullInput, $nullOutput); 40 | } 41 | 42 | protected function configure() 43 | { 44 | parent::configure(); 45 | 46 | $this->setName('auth'); 47 | $this->setDescription('Retrieve the Access Token for your Flickr application.'); 48 | 49 | $msg = 'Request authorisation even if the Access Token has already been stored.'; 50 | $this->addOption('force', 'f', InputOption::VALUE_NONE, $msg); 51 | } 52 | 53 | /** 54 | * @param InputInterface $input 55 | * @param OutputInterface $output 56 | */ 57 | protected function setup(InputInterface $input, OutputInterface $output) 58 | { 59 | $this->setIsConfigFileRequired(false); 60 | 61 | parent::setup($input, $output); 62 | 63 | $this->setupIo(); 64 | } 65 | 66 | private function setupIo() 67 | { 68 | $io = new SymfonyStyle($this->getInput(), $this->getOutput()); 69 | $this->io = $io; 70 | } 71 | 72 | /** 73 | * @param InputInterface $input 74 | * @param OutputInterface $output 75 | * @return int 76 | */ 77 | protected function execute(InputInterface $input, OutputInterface $output): int 78 | { 79 | $this->setup($input, $output); 80 | 81 | $configFilePath = $this->getConfigFilePath(); 82 | 83 | // Get the config file, or create one. 84 | try { 85 | $config = $this->loadConfig(); 86 | } catch (RuntimeException $exception) { 87 | $filesystem = new Filesystem(); 88 | if ($filesystem->exists($configFilePath)) { 89 | throw $exception; 90 | } 91 | 92 | // If we couldn't get the config, ask for the basic config values and then try again. 93 | $this->io->writeln('Go to https://www.flickr.com/services/apps/create/apply/ to create a new API key.'); 94 | $customerKey = $this->io->ask('Consumer key'); 95 | $customerSecret = $this->io->ask('Consumer secret'); 96 | 97 | $config = [ 98 | 'flickr' => [ 99 | 'consumer_key' => $customerKey, 100 | 'consumer_secret' => $customerSecret, 101 | ], 102 | ]; 103 | $this->saveConfig($config); 104 | 105 | // Fetch again, to make sure it's saved correctly. 106 | $config = $this->loadConfig(); 107 | } 108 | 109 | $hasToken = isset($config['flickr']['token']) && isset($config['flickr']['token_secret']); 110 | if (!$hasToken || $this->getInput()->hasOption('force') && $this->getInput()->getOption('force')) { 111 | $newConfig = $this->authenticate($configFilePath, $config['flickr']['consumer_key'], $config['flickr']['consumer_secret']); 112 | 113 | $config['flickr']['token'] = $newConfig['token']; 114 | $config['flickr']['token_secret'] = $newConfig['token_secret']; 115 | 116 | $this->io->success(sprintf('Saving config to %s', $configFilePath)); 117 | $this->saveConfig($config); 118 | } 119 | 120 | // Now test the stored credentials. 121 | $metadata = new Metadata($config['flickr']['consumer_key'], $config['flickr']['consumer_secret']); 122 | $metadata->setOauthAccess($config['flickr']['token'], $config['flickr']['token_secret']); 123 | 124 | $factory = new ApiFactory($metadata, new RezzzaGuzzleAdapter()); 125 | 126 | $this->io->text('Test Login'); 127 | $xml = $factory->call('flickr.test.login'); 128 | 129 | $attributes = $xml->attributes(); 130 | $stat = (string)$attributes->stat; 131 | 132 | if (strtolower($stat) == 'ok') { 133 | $this->io->success('Test Login successful'); 134 | } else { 135 | $this->io->text(sprintf('Status: %s', $stat)); 136 | } 137 | 138 | return $this->getExit(); 139 | } 140 | 141 | /** 142 | * Authenticate with Flickr. 143 | * 144 | * @param string $configPath The config filename. 145 | * @param string $customerKey 146 | * @param string $customerSecret 147 | * @return array 148 | */ 149 | protected function authenticate(string $configPath, string $customerKey, string $customerSecret) 150 | { 151 | $storage = new Memory(); 152 | 153 | // Out-of-band, i.e. no callback required for a CLI application. 154 | $credentials = new Credentials($customerKey, $customerSecret, 'oob'); 155 | 156 | $streamClient = new GuzzleStreamClient(); 157 | $signature = new Signature($credentials); 158 | 159 | $flickrService = new Flickr($credentials, $streamClient, $storage, $signature); 160 | $token = $flickrService->requestRequestToken(); 161 | if (!$token) { 162 | throw new RuntimeException('Request RequestToken failed.'); 163 | } 164 | 165 | $accessToken = $token->getAccessToken(); 166 | if (!$accessToken) { 167 | throw new RuntimeException('Cannot get Access Token.'); 168 | } 169 | 170 | $accessTokenSecret = $token->getAccessTokenSecret(); 171 | if (!$accessTokenSecret) { 172 | throw new RuntimeException('Cannot get Access Token Secret.'); 173 | } 174 | 175 | // Ask user for permissions. 176 | $permissions = $this->getPermissionType(); 177 | 178 | $additionalParameters = [ 179 | 'oauth_token' => $accessToken, 180 | 'perms' => $permissions, 181 | ]; 182 | $url = $flickrService->getAuthorizationUri($additionalParameters); 183 | 184 | $this->io->writeln(sprintf("Go to this URL to authorize FlickrCLI:\n\n%s", $url)); 185 | 186 | // Flickr says, at this point: 187 | // "You have successfully authorized the application XYZ to use your credentials. 188 | // You should now type this code into the application:" 189 | $question = 'Paste the 9-digit code (with or without hyphens) here:'; 190 | $verifier = $this->io->ask($question, null, function ($code) { 191 | $newCode = preg_replace('/[^0-9]/', '', $code); 192 | return $newCode; 193 | }); 194 | 195 | $token = $flickrService->requestAccessToken($token, $verifier, $accessTokenSecret); 196 | if (!$token) { 197 | throw new RuntimeException('Request AccessToken failed.'); 198 | } 199 | 200 | $accessToken = $token->getAccessToken(); 201 | $accessTokenSecret = $token->getAccessTokenSecret(); 202 | 203 | $newConfig = [ 204 | 'token' => $accessToken, 205 | 'token_secret' => $accessTokenSecret, 206 | ]; 207 | return $newConfig; 208 | } 209 | 210 | /** 211 | * Ask the user if they want to authenticate with read, write, or delete permissions. 212 | * 213 | * @return string The permission, one of 'read', write', or 'delete'. Defaults to 'read'. 214 | */ 215 | protected function getPermissionType(): string 216 | { 217 | $this->io->writeln('The permission you grant to FlickrCLI depends on what you want to do with it.'); 218 | 219 | $question = 'Please select from the following three options'; 220 | $choices = [ 221 | 'read' => 'download photos', 222 | 'write' => 'upload photos', 223 | 'delete' => 'download and/or delete photos from Flickr', 224 | ]; 225 | 226 | // Note that we're not currently setting a default here, because it is not yet possible 227 | // to set a non-numeric key as the default. https://github.com/symfony/symfony/issues/15032 228 | $permissions = $this->io->choice($question, $choices); 229 | return $permissions; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/TheFox/FlickrCli/Command/DeleteCommand.php: -------------------------------------------------------------------------------- 1 | setName('delete'); 18 | $this->setDescription('Delete Photosets.'); 19 | 20 | $this->addArgument('photosets', InputArgument::IS_ARRAY, 'Photosets to use.'); 21 | } 22 | 23 | /** 24 | * @param InputInterface $input 25 | * @param OutputInterface $output 26 | * @return int 27 | */ 28 | protected function execute(InputInterface $input, OutputInterface $output): int 29 | { 30 | parent::execute($input, $output); 31 | 32 | $photosets = $input->getArgument('photosets'); 33 | 34 | $apiService = $this->getApiService(); 35 | $apiFactory = $apiService->getApiFactory(); 36 | 37 | $photosetTitles = $apiService->getPhotosetTitles(); 38 | 39 | $this->getLogger()->notice('[main] start deleting files'); 40 | foreach ($photosetTitles as $photosetId => $photosetTitle) { 41 | pcntl_signal_dispatch(); 42 | if ($this->getExit()) { 43 | break; 44 | } 45 | 46 | if (!in_array($photosetTitle, $photosets)) { 47 | continue; 48 | } 49 | 50 | $xmlPhotoListOptions = [ 51 | 'photoset_id' => $photosetId, 52 | ]; 53 | $xmlPhotoList = $apiFactory->call('flickr.photosets.getPhotos', $xmlPhotoListOptions); 54 | $xmlPhotoListAttributes = $xmlPhotoList->photoset->attributes(); 55 | $xmlPhotoListPagesTotal = (int)$xmlPhotoListAttributes->pages; 56 | $xmlPhotoListPhotosTotal = (int)$xmlPhotoListAttributes->total; 57 | 58 | $this->getLogger()->info(sprintf('[photoset] %s: %s', $photosetTitle, $xmlPhotoListPhotosTotal)); 59 | 60 | $fileCount = 0; 61 | for ($page = 1; $page <= $xmlPhotoListPagesTotal; $page++) { 62 | pcntl_signal_dispatch(); 63 | if ($this->getExit()) { 64 | break; 65 | } 66 | 67 | if ($page > 1) { 68 | $xmlPhotoListOptions['page'] = $page; 69 | $xmlPhotoList = $apiFactory->call('flickr.photosets.getPhotos', $xmlPhotoListOptions); 70 | } 71 | 72 | /** 73 | * @var int $n 74 | * @var SimpleXMLElement $photo 75 | */ 76 | foreach ($xmlPhotoList->photoset->photo as $n => $photo) { 77 | pcntl_signal_dispatch(); 78 | if ($this->getExit()) { 79 | break; 80 | } 81 | 82 | $fileCount++; 83 | $id = (string)$photo->attributes()->id; 84 | try { 85 | $apiFactory->call('flickr.photos.delete', ['photo_id' => $id]); 86 | $this->getLogger()->info(sprintf('[photo] %d/%d deleted %s', $page, $fileCount, $id)); 87 | } catch (Exception $e) { 88 | $this->getLogger()->info(sprintf('[photo] %d/%d delete %s FAILED: %s', $page, $fileCount, $id, $e->getMessage())); 89 | } 90 | } 91 | } 92 | } 93 | 94 | return $this->getExit(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/TheFox/FlickrCli/Command/DownloadCommand.php: -------------------------------------------------------------------------------- 1 | setName('download'); 64 | $this->setDescription('Download files from Flickr.'); 65 | 66 | $this->addOption('destination', 'd', InputOption::VALUE_OPTIONAL, 'Path to save files. Default: photosets'); 67 | 68 | $idDirsDescr = 'Save downloaded files into ID-based directories. Default is to group by Album titles instead.'; 69 | $this->addOption('id-dirs', 'i', InputOption::VALUE_NONE, $idDirsDescr); 70 | 71 | $forceDescr = 'Force Flickr CLI to download photos even if they already exist locally. '; 72 | $forceDescr .= 'Default is to skip existing downloads.'; 73 | $this->addOption('force', 'f', InputOption::VALUE_NONE, $forceDescr); 74 | 75 | $this->addArgument('photosets', InputArgument::IS_ARRAY, 'Photosets to download.'); 76 | 77 | $this->destinationPath = 'photosets'; 78 | } 79 | 80 | private function setupDestination() 81 | { 82 | $filesystem = new Filesystem(); 83 | 84 | // Destination directory. Default to 'photosets'. 85 | $customDestDir = $this->getInput()->getOption('destination'); 86 | if (!empty($customDestDir)) { 87 | $this->destinationPath = rtrim($customDestDir, '/'); 88 | } 89 | if (!$filesystem->exists($this->destinationPath)) { 90 | $filesystem->mkdir($this->destinationPath, 0755); 91 | } 92 | } 93 | 94 | /** 95 | * Executes the download command. 96 | * 97 | * @param InputInterface $input An InputInterface instance 98 | * @param OutputInterface $output An OutputInterface instance 99 | * @return int 0 if everything went fine, or an error code. 100 | */ 101 | protected function execute(InputInterface $input, OutputInterface $output): int 102 | { 103 | parent::execute($input, $output); 104 | 105 | $this->setupDestination(); 106 | 107 | // Force download? 108 | $this->forceDownload = $input->getOption('force'); 109 | 110 | // Run the actual download. 111 | if ($input->getOption('id-dirs')) { 112 | // If downloaded files should be saved into download-dir/hash/hash/photo-id/ directories. 113 | $exit = $this->downloadById(); 114 | } else { 115 | // If download directories should match Album titles. 116 | $exit = $this->downloadByAlbumTitle(); 117 | } 118 | 119 | return $exit; 120 | } 121 | 122 | /** 123 | * Download photos to directories named after the album (i.e. photoset, in the original parlance). 124 | * 125 | * @return int 126 | */ 127 | protected function downloadByAlbumTitle(): int 128 | { 129 | $this->getLogger()->info(sprintf('Downloading to Album-based directories in: %s', $this->destinationPath)); 130 | 131 | $apiService = $this->getApiService(); 132 | $apiFactory = $apiService->getApiFactory(); 133 | $xml = $apiFactory->call('flickr.photosets.getList'); 134 | 135 | $photosets = $this->getInput()->getArgument('photosets'); 136 | if (!is_array($photosets)) { 137 | throw new RuntimeException('photosets is not an array'); 138 | } 139 | 140 | $photosetsInUse = []; 141 | if (count($photosets)) { 142 | $photosetTitles = $apiService->getPhotosetTitles(); 143 | 144 | foreach ($photosets as $argPhotosetTitle) { 145 | pcntl_signal_dispatch(); 146 | if ($this->getExit()) { 147 | break; 148 | } 149 | 150 | if (!in_array($argPhotosetTitle, $photosetTitles)) { 151 | continue; 152 | } 153 | 154 | $photosetsInUse[] = $argPhotosetTitle; 155 | } 156 | 157 | foreach ($photosets as $argPhotosetTitle) { 158 | pcntl_signal_dispatch(); 159 | if ($this->getExit()) { 160 | break; 161 | } 162 | 163 | if (in_array($argPhotosetTitle, $photosetsInUse)) { 164 | continue; 165 | } 166 | 167 | foreach ($photosetTitles as $photosetTitle) { 168 | if (!fnmatch($argPhotosetTitle, $photosetTitle)) { 169 | continue; 170 | } 171 | 172 | $photosetsInUse[] = $photosetTitle; 173 | } 174 | } 175 | } else { 176 | foreach ($xml->photosets->photoset as $photoset) { 177 | pcntl_signal_dispatch(); 178 | if ($this->getExit()) { 179 | break; 180 | } 181 | 182 | $photosetsInUse[] = $photoset->title; 183 | } 184 | } 185 | 186 | $filesystem = new Filesystem(); 187 | $totalDownloaded = 0; 188 | $totalFiles = 0; 189 | 190 | /** @var $photoset SimpleXMLElement */ 191 | foreach ($xml->photosets->photoset as $photoset) { 192 | pcntl_signal_dispatch(); 193 | if ($this->getExit()) { 194 | break; 195 | } 196 | 197 | if (!in_array($photoset->title, $photosetsInUse)) { 198 | continue; 199 | } 200 | 201 | $photosetId = (int)$photoset->attributes()->id; 202 | $photosetTitle = (string)$photoset->title; 203 | $this->getLogger()->info(sprintf('[photoset] %s', $photosetTitle)); 204 | 205 | $destinationPath = sprintf('%s/%s', $this->destinationPath, $photosetTitle); 206 | 207 | if (!$filesystem->exists($destinationPath)) { 208 | $this->getLogger()->info(sprintf('[dir] create: %s', $destinationPath)); 209 | $filesystem->mkdir($destinationPath); 210 | } 211 | 212 | $this->getLogger()->info(sprintf('[photoset] %s: get photo list', $photosetTitle)); 213 | $xmlPhotoList = $apiFactory->call('flickr.photosets.getPhotos', [ 214 | 'photoset_id' => $photosetId, 215 | ]); 216 | $xmlPhotoListPagesTotal = (int)$xmlPhotoList->photoset->attributes()->pages; 217 | // $xmlPhotoListPhotosTotal = (int)$xmlPhotoList->photoset->attributes()->total; 218 | 219 | $fileCount = 0; 220 | 221 | for ($page = 1; $page <= $xmlPhotoListPagesTotal; $page++) { 222 | pcntl_signal_dispatch(); 223 | if ($this->getExit()) { 224 | break; 225 | } 226 | 227 | $this->getLogger()->info(sprintf('[page] %d', $page)); 228 | 229 | if ($page > 1) { 230 | $this->getLogger()->info(sprintf('[photoset] %s: get photo list', $photosetTitle)); 231 | $xmlPhotoList = $apiFactory->call('flickr.photosets.getPhotos', [ 232 | 'photoset_id' => $photosetId, 233 | 'page' => $page, 234 | ]); 235 | } 236 | 237 | /** @var $photo SimpleXMLElement */ 238 | foreach ($xmlPhotoList->photoset->photo as $photo) { 239 | pcntl_signal_dispatch(); 240 | if ($this->getExit()) { 241 | break; 242 | } 243 | 244 | $this->getLogger()->debug(sprintf('[media] %d/%d photo %s', $page, $fileCount, $photo['id'])); 245 | $downloaded = $this->downloadPhoto($photo, $destinationPath); 246 | if ($downloaded && isset($downloaded->filesize)) { 247 | $totalDownloaded += $downloaded->filesize; 248 | } 249 | $fileCount++; 250 | } 251 | } 252 | } 253 | 254 | if ($totalDownloaded > 0) { 255 | $bytesize = new ByteSize(); 256 | $totalDownloadedMsg = $bytesize->format($totalDownloaded); 257 | } else { 258 | $totalDownloadedMsg = 0; 259 | } 260 | 261 | $this->getLogger()->info(sprintf('[main] total downloaded: %d', $totalDownloadedMsg)); 262 | $this->getLogger()->info(sprintf('[main] total files: %d', $totalFiles)); 263 | $this->getLogger()->info('[main] exit'); 264 | 265 | return $this->getExit(); 266 | } 267 | 268 | /** 269 | * Download a single given photo from Flickr. Won't be downloaded if already exists locally; if it is downloaded the 270 | * additional 'filesize' property will be set on the return element. 271 | * 272 | * @param SimpleXMLElement $photo 273 | * @param string $destinationPath 274 | * @param string $basename The filename to save the downloaded file to (without extension). 275 | * @return SimpleXMLElement|boolean Photo metadata as returned by Flickr, or false if something went wrong. 276 | * @throws Exception 277 | */ 278 | private function downloadPhoto(SimpleXMLElement $photo, string $destinationPath, string $basename = null) 279 | { 280 | $id = (string)$photo->attributes()->id; 281 | 282 | $apiFactory = $this->getApiService()->getApiFactory(); 283 | 284 | try { 285 | $xmlPhoto = $apiFactory->call('flickr.photos.getInfo', [ 286 | 'photo_id' => $id, 287 | 'secret' => (string)$photo->attributes()->secret, 288 | ]); 289 | if (!$xmlPhoto) { 290 | return false; 291 | } 292 | } catch (Exception $e) { 293 | $this->getLogger()->error(sprintf( 294 | '%s, GETINFO FAILED: %s', 295 | $id, 296 | $e->getMessage() 297 | )) 298 | ; 299 | 300 | return false; 301 | } 302 | 303 | if (isset($xmlPhoto->photo->title) && (string)$xmlPhoto->photo->title) { 304 | $title = (string)$xmlPhoto->photo->title; 305 | } else { 306 | $title = ''; 307 | } 308 | 309 | $server = (string)$xmlPhoto->photo->attributes()->server; 310 | $farm = (string)$xmlPhoto->photo->attributes()->farm; 311 | $originalSecret = (string)$xmlPhoto->photo->attributes()->originalsecret; 312 | $originalFormat = (string)$xmlPhoto->photo->attributes()->originalformat; 313 | $description = (string)$xmlPhoto->photo->description; 314 | $media = (string)$xmlPhoto->photo->attributes()->media; 315 | //$ownerPathalias = (string)$xmlPhoto->photo->owner->attributes()->path_alias; 316 | //$ownerNsid = (string)$xmlPhoto->photo->owner->attributes()->nsid; 317 | 318 | // Set the filename. 319 | if (empty($basename)) { 320 | $fileName = sprintf('%s.%s', $title ? $title : $id, $originalFormat); 321 | } else { 322 | $fileName = sprintf('%s.%s', $basename, $originalFormat); 323 | } 324 | $filePath = sprintf('%s/%s', rtrim($destinationPath, '/'), $fileName); 325 | $filePathTmp = sprintf('%s/%s.%s.tmp', $destinationPath, $id, $originalFormat); 326 | 327 | $filesystem = new Filesystem(); 328 | if ($filesystem->exists($filePath) && !$this->forceDownload) { 329 | $this->getLogger()->debug(sprintf('File %s already downloaded to %s', $id, $filePath)); 330 | 331 | /** @var SimpleXMLElement $photo */ 332 | $photo = $xmlPhoto->photo; 333 | 334 | return $photo; 335 | } 336 | 337 | // URL format for the original image. See https://www.flickr.com/services/api/misc.urls.html 338 | // https://farm{farm-id}.staticflickr.com/{server-id}/{id}_{o-secret}_o.(jpg|gif|png) 339 | $urlFormat = 'https://farm%s.staticflickr.com/%s/%s_%s_o.%s'; 340 | $url = sprintf($urlFormat, $farm, $server, $id, $originalSecret, $originalFormat); 341 | 342 | if ($media == 'video') { 343 | // $url = 'http://www.flickr.com/photos/'.$ownerPathalias.'/'.$id.'/play/orig/'.$originalSecret.'/'; 344 | // $url = 'https://www.flickr.com/video_download.gne?id='.$id; 345 | 346 | // $contentDispositionHeaderArray = array(); 347 | 348 | // try{ 349 | // $client = new GuzzleHttpClient(); 350 | // $request = $client->head($url); 351 | // $response = $request->send(); 352 | 353 | // $url = $response->getEffectiveUrl(); 354 | 355 | // $contentDispositionHeader = $response->getHeader('content-disposition'); 356 | // $contentDispositionHeaderArray = $contentDispositionHeader->toArray(); 357 | // } 358 | // catch(Exception $e){ 359 | // $this->log->info(sprintf('[%s] %s, farm %s, server %s, %s HEAD FAILED: %s', 360 | // $media, $id, $farm, $server, $fileName, $e->getMessage())); 361 | // $this->logFilesFailed->error($id.'.'.$originalFormat); 362 | 363 | // continue; 364 | // } 365 | 366 | // if(count($contentDispositionHeaderArray)){ 367 | // $pos = strpos(strtolower($contentDispositionHeaderArray[0]), 'filename='); 368 | // if($pos !== false){ 369 | // $pathinfo = pathinfo(substr($contentDispositionHeaderArray[0], $pos + 9)); 370 | // if(isset($pathinfo['extension'])){ 371 | // $originalFormat = $pathinfo['extension']; 372 | // $fileName = ($title ? $title : $id).'.'.$originalFormat; 373 | // $filePath = $dstDirFullPath.'/'.$fileName; 374 | // $filePathTmp = $dstDirFullPath.'/'.$id.'.'.$originalFormat.'.tmp'; 375 | 376 | // if($filesystem->exists($filePath)){ 377 | // continue; 378 | // } 379 | // } 380 | // } 381 | // } 382 | 383 | $this->getLogger()->error('video not supported yet'); 384 | //$this->loggerFilesFailed->error($id . ': video not supported yet'); 385 | return false; 386 | } 387 | 388 | $client = new GuzzleHttpClient($url); 389 | 390 | $streamRequestFactory = new PhpStreamRequestFactory(); 391 | try { 392 | $request = $client->get(); 393 | $stream = $streamRequestFactory->fromRequest($request); 394 | } catch (Exception $e) { 395 | $this->getLogger()->error(sprintf( 396 | '[%s] %s, farm %s, server %s, %s FAILED: %s', 397 | $media, 398 | $id, 399 | $farm, 400 | $server, 401 | $fileName, 402 | $e->getMessage() 403 | )) 404 | ; 405 | //$this->loggerFilesFailed->error($id . '.' . $originalFormat); 406 | 407 | return false; 408 | } 409 | 410 | $size = $stream->getSize(); 411 | if (false !== $size) { 412 | $bytesize = new ByteSize(); 413 | $sizeStr = $bytesize->format((int)$size); 414 | } else { 415 | $sizeStr = 'N/A'; 416 | } 417 | 418 | $this->getLogger()->info(sprintf( 419 | "[%s] %s, farm %s, server %s, %s, '%s', %s", 420 | $media, 421 | $id, 422 | $farm, 423 | $server, 424 | $fileName, 425 | $description, 426 | $sizeStr 427 | )) 428 | ; 429 | 430 | $timePrev = time(); 431 | $downloaded = 0; 432 | $downloadedPrev = 0; 433 | $downloadedDiff = 0; 434 | 435 | $fh = fopen($filePathTmp, 'wb'); 436 | if (false === $fh) { 437 | throw new RuntimeException(sprintf('Unable to open %s for writing.', $filePathTmp)); 438 | } 439 | while (!$stream->feof()) { 440 | pcntl_signal_dispatch(); 441 | if ($this->getExit()) { 442 | break; 443 | } 444 | 445 | $data = $stream->read(FlickrCli::DOWNLOAD_STREAM_READ_LEN); 446 | $dataLen = strlen($data); 447 | fwrite($fh, $data); 448 | 449 | $downloaded += $dataLen; 450 | 451 | if ($size !== false) { 452 | $percent = $downloaded / $size * 100; 453 | } else { 454 | $percent = 0; 455 | } 456 | if ($percent > 100) { 457 | $percent = 100; 458 | } 459 | 460 | $progressbarDownloaded = round($percent / 100 * FlickrCli::DOWNLOAD_PROGRESSBAR_ITEMS); 461 | $progressbarRest = FlickrCli::DOWNLOAD_PROGRESSBAR_ITEMS - $progressbarDownloaded; 462 | 463 | $timeCur = time(); 464 | if ($timeCur != $timePrev) { 465 | $timePrev = $timeCur; 466 | $downloadedDiff = $downloaded - $downloadedPrev; 467 | $downloadedPrev = $downloaded; 468 | } 469 | 470 | $downloadedDiffStr = ''; 471 | if ($downloadedDiff) { 472 | $bytesize = new ByteSize(); 473 | $downloadedDiffStr = $bytesize->format($downloadedDiff) . '/s'; 474 | } 475 | 476 | if ($size !== false) { 477 | // If we know the stream size, show a progress bar. 478 | printf( 479 | "[file] %6.2f%% [%s%s] %s %10s\x1b[0K\r", 480 | $percent, 481 | str_repeat('#', $progressbarDownloaded), 482 | str_repeat(' ', $progressbarRest), 483 | number_format($downloaded), 484 | $downloadedDiffStr 485 | ); 486 | } else { 487 | // Otherwise, just show the amount downloaded and speed. 488 | printf("[file] %s %10s\x1b[0K\r", number_format($downloaded), $downloadedDiffStr); 489 | } 490 | } 491 | fclose($fh); 492 | print "\n"; 493 | 494 | $fileTmpSize = filesize($filePathTmp); 495 | 496 | if ($this->getExit()) { 497 | $filesystem->remove($filePathTmp); 498 | } elseif (($size && $fileTmpSize != $size) || $fileTmpSize <= 1024) { 499 | $filesystem->remove($filePathTmp); 500 | 501 | $this->getLogger()->error(sprintf('[%s] %s FAILED: temp file size wrong: %d', $media, $id, $fileTmpSize)); 502 | } else { 503 | // Rename to its final destination, and return the photo metadata. 504 | $filesystem->rename($filePathTmp, $filePath, $this->forceDownload); 505 | $xmlPhoto->photo->filesize = $size; 506 | 507 | /** @var SimpleXMLElement $photo */ 508 | $photo = $xmlPhoto->photo; 509 | 510 | return $photo; 511 | } 512 | 513 | return false; 514 | } 515 | 516 | /** 517 | * Download all photos, whether in a set/album or not, into directories named by photo ID. 518 | */ 519 | private function downloadById() 520 | { 521 | $this->getLogger()->info(sprintf('Downloading to ID-based directories in: %s', $this->destinationPath)); 522 | 523 | $apiFactory = $this->getApiService()->getApiFactory(); 524 | 525 | // 1. Download any photos not in a set. 526 | $notInSetPage = 1; 527 | do { 528 | $notInSet = $apiFactory->call('flickr.photos.getNotInSet', ['page' => $notInSetPage]); 529 | $pages = (int)$notInSet->photos['pages']; 530 | $this->getLogger()->info(sprintf('Not in set p%s/%d', $notInSetPage, $pages)); 531 | 532 | $notInSetPage++; 533 | foreach ($notInSet->photos->photo as $photo) { 534 | $this->downloadPhotoById($photo); 535 | } 536 | } while ($notInSetPage <= $notInSet->photos['pages']); 537 | 538 | // 2. Download all photos in all sets. 539 | $setsPage = 1; 540 | do { 541 | $sets = $apiFactory->call('flickr.photosets.getList', ['page' => $setsPage]); 542 | $pages = (int)$sets->photosets['pages']; 543 | $this->getLogger()->info(sprintf('Sets p%d/%d', $setsPage, $pages)); 544 | 545 | foreach ($sets->photosets->photoset as $set) { 546 | // Loop through all pages in this set. 547 | $setPhotosPage = 1; 548 | do { 549 | $params = [ 550 | 'photoset_id' => $set['id'], 551 | 'page' => $setPhotosPage, 552 | ]; 553 | $setPhotos = $apiFactory->call('flickr.photosets.getPhotos', $params); 554 | 555 | $title = (string)$set->title; 556 | $total = (int)$setPhotos->photoset['total']; 557 | $setPages = (int)$setPhotos->photoset['pages']; 558 | 559 | $this->getLogger()->info(sprintf( 560 | '[Set %s] %s photos (p%s/%s)', 561 | $title, 562 | $total, 563 | $setPhotosPage, 564 | $setPages 565 | )) 566 | ; 567 | foreach ($setPhotos->photoset->photo as $photo) { 568 | $this->downloadPhotoById($photo); 569 | } 570 | $setPhotosPage++; 571 | } while ($setPhotosPage <= $setPhotos->photos['pages']); 572 | } 573 | $setsPage++; 574 | } while ($setsPage <= (int)$sets->photosets['pages']); 575 | 576 | return $this->getExit(); 577 | } 578 | 579 | /** 580 | * Download a single photo. 581 | * 582 | * @param SimpleXMLElement $photo Basic photo metadata. 583 | */ 584 | private function downloadPhotoById(SimpleXMLElement $photo) 585 | { 586 | $id = $photo['id']; 587 | $idHash = md5($id); 588 | $destinationPath = sprintf('%s/%s/%s/%s/%s/%s', $this->destinationPath, $idHash[0], $idHash[1], $idHash[2], $idHash[3], $id); 589 | 590 | $filesystem = new Filesystem(); 591 | if (!$filesystem->exists($destinationPath)) { 592 | $filesystem->mkdir($destinationPath, 0755); 593 | } 594 | 595 | // Save the actual file. 596 | $apiFactory = $this->getApiService()->getApiFactory(); 597 | $photo = $this->downloadPhoto($photo, $destinationPath, $id); 598 | if (false === $photo) { 599 | $this->getLogger()->error(sprintf('Unable to get metadata about photo: %s', $id)); 600 | return; 601 | } 602 | 603 | $fn = $this->getMappingFunction($apiFactory); 604 | 605 | $metadata = $fn($photo); 606 | 607 | $content = Yaml::dump($metadata); 608 | $filesystem->dumpFile(sprintf('%s/metadata.yml', $destinationPath), $content); 609 | } 610 | 611 | /** 612 | * @param ApiFactory $apiFactory 613 | * @return \Closure 614 | */ 615 | private function getMappingFunction(ApiFactory $apiFactory) 616 | { 617 | /** 618 | * @param SimpleXMLElement $photo 619 | * @return array 620 | */ 621 | $fn = function (SimpleXMLElement $photo) use ($apiFactory) { 622 | // Metadata 623 | $metadataFn = $this->getMetadataMappingFunction(); 624 | $metadata = $metadataFn($photo); 625 | 626 | if (isset($photo->photo->description->_content)) { 627 | $metadata['description'] = (string)$photo->photo->description->_content; 628 | } 629 | 630 | // Tags 631 | if (isset($photo->tags->tag)) { 632 | //$tagsFn = $this->getTagMappingFunction(); 633 | //$tags = (array)$photo->tags; 634 | //$metadata['tags'] = array_map($tagsFn, $tags); 635 | 636 | foreach ($photo->tags->tag as $tag) { 637 | $metadata['tags'][] = [ 638 | 'id' => (string)$tag['id'], 639 | 'slug' => (string)$tag, 640 | 'title' => (string)$tag['raw'], 641 | 'machine' => $tag['machine_tag'] !== '0', 642 | ]; 643 | } 644 | } 645 | 646 | // Location 647 | if (isset($photo->location)) { 648 | $metadata['location'] = [ 649 | 'latitude' => (float)$photo->location['latitude'], 650 | 'longitude' => (float)$photo->location['longitude'], 651 | 'accuracy' => (integer)$photo->location['accuracy'], 652 | ]; 653 | } 654 | 655 | // Contexts 656 | $contexts = $apiFactory->call('flickr.photos.getAllContexts', ['photo_id' => $photo['id']]); 657 | foreach ($contexts->set as $set) { 658 | $metadata['sets'][] = [ 659 | 'id' => (string)$set['id'], 660 | 'title' => (string)$set['title'], 661 | ]; 662 | } 663 | 664 | // Pools 665 | foreach ($contexts->pool as $pool) { 666 | $metadata['pools'][] = [ 667 | 'id' => (string)$pool['id'], 668 | 'title' => (string)$pool['title'], 669 | 'url' => (string)$pool['url'], 670 | ]; 671 | } 672 | 673 | return $metadata; 674 | }; 675 | 676 | return $fn; 677 | } 678 | 679 | /** 680 | * @return \Closure 681 | */ 682 | private function getMetadataMappingFunction() 683 | { 684 | /** 685 | * @param SimpleXMLElement $photo 686 | * @return array 687 | */ 688 | $fn = function (SimpleXMLElement $photo) { 689 | $metadata = [ 690 | 'id' => (int)$photo['id'], 691 | 'title' => (string)$photo->title, 692 | 'license' => (string)$photo['license'], 693 | 'safety_level' => (string)$photo['safety_level'], 694 | 'rotation' => (string)$photo['rotation'], 695 | 'media' => (string)$photo['media'], 696 | 'format' => (string)$photo['originalformat'], 697 | 'owner' => [ 698 | 'nsid' => (string)$photo->owner['nsid'], 699 | 'username' => (string)$photo->owner['username'], 700 | 'realname' => (string)$photo->owner['realname'], 701 | 'path_alias' => (string)$photo->owner['path_alias'], 702 | ], 703 | 'visibility' => [ 704 | 'ispublic' => (boolean)$photo->visibility['ispublic'], 705 | 'isfriend' => (boolean)$photo->visibility['isfriend'], 706 | 'isfamily' => (boolean)$photo->visibility['isfamily'], 707 | ], 708 | 'dates' => [ 709 | 'posted' => (string)$photo->dates['posted'], 710 | 'taken' => (string)$photo->dates['taken'], 711 | 'takengranularity' => (int)$photo->dates['takengranularity'], 712 | 'takenunknown' => (string)$photo->dates['takenunknown'], 713 | 'lastupdate' => (string)$photo->dates['lastupdate'], 714 | 'uploaded' => (string)$photo['dateuploaded'], 715 | ], 716 | 'tags' => [], 717 | 'sets' => [], 718 | 'pools' => [], 719 | ]; 720 | return $metadata; 721 | }; 722 | 723 | return $fn; 724 | } 725 | } 726 | -------------------------------------------------------------------------------- /src/TheFox/FlickrCli/Command/FilesCommand.php: -------------------------------------------------------------------------------- 1 | setName('files'); 28 | $this->setDescription('List Files.'); 29 | 30 | $this->addOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Path to config file. Default: config.yml'); 31 | $this->addArgument('photosets', InputArgument::IS_ARRAY, 'Photosets to use.'); 32 | } 33 | 34 | /** 35 | * @param InputInterface $input 36 | * @param OutputInterface $output 37 | * @return int 38 | */ 39 | protected function execute(InputInterface $input, OutputInterface $output): int 40 | { 41 | parent::execute($input, $output); 42 | 43 | $photosets = $input->getArgument('photosets'); 44 | 45 | $apiService = $this->getApiService(); 46 | $apiFactory = $apiService->getApiFactory(); 47 | 48 | $photosetTitles = $apiService->getPhotosetTitles(); 49 | foreach ($photosetTitles as $photosetId => $photosetTitle) { 50 | pcntl_signal_dispatch(); 51 | if ($this->getExit()) { 52 | break; 53 | } 54 | 55 | if (!in_array($photosetTitle, $photosets)) { 56 | continue; 57 | } 58 | 59 | $xmlPhotoListOptions = ['photoset_id' => $photosetId]; 60 | $xmlPhotoList = $apiFactory->call('flickr.photosets.getPhotos', $xmlPhotoListOptions); 61 | $xmlPhotoListPagesTotal = (int)$xmlPhotoList->photoset->attributes()->pages; 62 | $xmlPhotoListPhotosTotal = (int)$xmlPhotoList->photoset->attributes()->total; 63 | 64 | printf('%s (%d)' . "\n", $photosetTitle, $xmlPhotoListPhotosTotal); 65 | 66 | $fileCount = 0; 67 | 68 | for ($page = 1; $page <= $xmlPhotoListPagesTotal; $page++) { 69 | pcntl_signal_dispatch(); 70 | if ($this->getExit()) { 71 | break; 72 | } 73 | 74 | if ($page > 1) { 75 | $xmlPhotoListOptions['page'] = $page; 76 | $xmlPhotoList = $apiFactory->call('flickr.photosets.getPhotos', $xmlPhotoListOptions); 77 | } 78 | 79 | /** 80 | * @var int $n 81 | * @var SimpleXMLElement $photo 82 | */ 83 | foreach ($xmlPhotoList->photoset->photo as $n => $photo) { 84 | pcntl_signal_dispatch(); 85 | if ($this->getExit()) { 86 | break; 87 | } 88 | 89 | $id = (string)$photo->attributes()->id; 90 | $fileCount++; 91 | 92 | printf(' %d/%d %s' . "\n", $page, $fileCount, $id); 93 | } 94 | } 95 | } 96 | 97 | return $this->getExit(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/TheFox/FlickrCli/Command/FlickrCliCommand.php: -------------------------------------------------------------------------------- 1 | exit = 0; 74 | $this->logger = new NullLogger(); 75 | $this->output = new NullOutput(); 76 | $this->configFilePath = 'config.yml'; 77 | $this->isConfigFileRequired = true; 78 | $this->config = []; 79 | } 80 | 81 | /** 82 | * @return int 83 | */ 84 | public function getExit(): int 85 | { 86 | return $this->exit; 87 | } 88 | 89 | /** 90 | * @param int $exit 91 | */ 92 | public function setExit(int $exit) 93 | { 94 | $this->exit = $exit; 95 | } 96 | 97 | /** 98 | * @return LoggerInterface 99 | */ 100 | public function getLogger(): LoggerInterface 101 | { 102 | return $this->logger; 103 | } 104 | 105 | /** 106 | * @return InputInterface 107 | */ 108 | public function getInput(): InputInterface 109 | { 110 | return $this->input; 111 | } 112 | 113 | /** 114 | * @return OutputInterface 115 | */ 116 | public function getOutput(): OutputInterface 117 | { 118 | return $this->output; 119 | } 120 | 121 | /** 122 | * @return string 123 | */ 124 | public function getConfigFilePath(): string 125 | { 126 | return $this->configFilePath; 127 | } 128 | 129 | /** 130 | * @return bool 131 | */ 132 | public function isConfigFileRequired(): bool 133 | { 134 | return $this->isConfigFileRequired; 135 | } 136 | 137 | /** 138 | * @param bool $isConfigFileRequired 139 | */ 140 | public function setIsConfigFileRequired(bool $isConfigFileRequired) 141 | { 142 | $this->isConfigFileRequired = $isConfigFileRequired; 143 | } 144 | 145 | /** 146 | * @return array 147 | */ 148 | public function getConfig(): array 149 | { 150 | return $this->config; 151 | } 152 | 153 | /** 154 | * @param array $config 155 | */ 156 | public function setConfig(array $config) 157 | { 158 | $this->config = $config; 159 | } 160 | 161 | /** 162 | * @return ApiService 163 | */ 164 | public function getApiService(): ApiService 165 | { 166 | return $this->apiService; 167 | } 168 | 169 | /** 170 | * Configure the command. 171 | * This adds the standard 'config' and 'log' options that are common to all Flickr CLI commands. 172 | */ 173 | protected function configure() 174 | { 175 | $this 176 | ->addOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Path to config file. Default: ./config.yml')//->addOption('log', 'l', InputOption::VALUE_OPTIONAL, 'Path to log directory. Default: ./log') 177 | ; 178 | } 179 | 180 | /** 181 | * @param InputInterface $input 182 | * @param OutputInterface $output 183 | */ 184 | protected function setup(InputInterface $input, OutputInterface $output) 185 | { 186 | $this->input = $input; 187 | $this->output = $output; 188 | 189 | $this->signalHandlerSetup(); 190 | $this->setupLogger(); 191 | 192 | $this->setupConfig(); 193 | 194 | $this->setupServices(); 195 | } 196 | 197 | private function setupLogger() 198 | { 199 | $this->logger = new Logger($this->getName()); 200 | 201 | switch ($this->output->getVerbosity()) { 202 | case OutputInterface::VERBOSITY_DEBUG: // -vvv 203 | $logLevel = Logger::DEBUG; 204 | break; 205 | 206 | case OutputInterface::VERBOSITY_VERY_VERBOSE: // -vv 207 | $logLevel = Logger::INFO; 208 | break; 209 | 210 | case OutputInterface::VERBOSITY_VERBOSE: // -v 211 | $logLevel = Logger::NOTICE; 212 | break; 213 | 214 | case OutputInterface::VERBOSITY_QUIET: 215 | $logLevel = Logger::ERROR; 216 | break; 217 | 218 | case OutputInterface::VERBOSITY_NORMAL: 219 | default: 220 | $logLevel = Logger::WARNING; 221 | } 222 | 223 | //$logFormatter = new LineFormatter("[%datetime%] %level_name%: %message%\n"); 224 | 225 | $handler = new StreamHandler('php://stdout', $logLevel); 226 | //$handler->setFormatter($logFormatter); 227 | $this->logger->pushHandler($handler); 228 | } 229 | 230 | private function setupConfig() 231 | { 232 | $input = $this->getInput(); 233 | 234 | if ($input->hasOption('config') && $input->getOption('config')) { 235 | $configFilePath = $input->getOption('config'); 236 | } elseif ($envConfigFile = getenv('FLICKRCLI_CONFIG')) { 237 | $configFilePath = $envConfigFile; 238 | } 239 | 240 | if (!isset($configFilePath) || !$configFilePath) { 241 | throw new RuntimeException('No config file path found.'); 242 | } 243 | $this->configFilePath = $configFilePath; 244 | 245 | $filesystem = new Filesystem(); 246 | if ($filesystem->exists($this->configFilePath)) { 247 | $this->loadConfig(); 248 | } elseif ($this->isConfigFileRequired()) { 249 | throw new RuntimeException(sprintf('Config file not found: %s', $this->configFilePath)); 250 | } 251 | } 252 | 253 | /** 254 | * @return array 255 | */ 256 | public function loadConfig(): array 257 | { 258 | $configFilePath = $this->getConfigFilePath(); 259 | if (!$configFilePath) { 260 | throw new RuntimeException('Config File Path is not set.'); 261 | } 262 | 263 | $this->getLogger()->debug(sprintf('Load configuration: %s', $this->getConfigFilePath())); 264 | 265 | /** @var string[][] $config */ 266 | $config = Yaml::parse($configFilePath); 267 | 268 | if (!isset($config) 269 | || !isset($config['flickr']) 270 | || !isset($config['flickr']['consumer_key']) 271 | || !isset($config['flickr']['consumer_secret']) 272 | ) { 273 | throw new RuntimeException('Invalid configuration file.'); 274 | } 275 | 276 | $this->config = $config; 277 | return $this->config; 278 | } 279 | 280 | /** 281 | * @param array|null $config 282 | */ 283 | public function saveConfig(array $config = null) 284 | { 285 | if ($config) { 286 | $this->setConfig($config); 287 | } else { 288 | $config = $this->getConfig(); 289 | } 290 | 291 | $configContent = Yaml::dump($config); 292 | 293 | $configFilePath = $this->getConfigFilePath(); 294 | 295 | $filesystem = new Filesystem(); 296 | $filesystem->touch($configFilePath); 297 | $filesystem->chmod($configFilePath, 0600); 298 | $filesystem->dumpFile($configFilePath, $configContent); 299 | } 300 | 301 | /** 302 | * @return bool 303 | */ 304 | private function setupServices() 305 | { 306 | $config = $this->getConfig(); 307 | if (!$config) { 308 | return false; 309 | } 310 | 311 | if (!array_key_exists('flickr', $config)) { 312 | return false; 313 | } 314 | 315 | $consumerKey = $config['flickr']['consumer_key']; 316 | if (!$consumerKey) { 317 | return false; 318 | } 319 | 320 | $consumerSecret = $config['flickr']['consumer_secret']; 321 | if (!$consumerSecret) { 322 | return false; 323 | } 324 | 325 | $token = $config['flickr']['token']; 326 | if (!$token) { 327 | return false; 328 | } 329 | 330 | $tokenSecret = $config['flickr']['token_secret']; 331 | if (!$tokenSecret) { 332 | return false; 333 | } 334 | 335 | $this->apiService = new ApiService($consumerKey, $consumerSecret, $token, $tokenSecret); 336 | $this->apiService->setLogger($this->logger); 337 | 338 | return true; 339 | } 340 | 341 | private function signalHandlerSetup() 342 | { 343 | if (!function_exists('pcntl_signal') || !function_exists('pcntl_signal_dispatch')) { 344 | throw new SignalException('pcntl_signal function not found. You need to install pcntl PHP extention.'); 345 | } 346 | 347 | declare(ticks=1); 348 | 349 | pcntl_signal(SIGTERM, [$this, 'signalHandler']); 350 | /** @uses $this::signalHandler() */ 351 | pcntl_signal(SIGINT, [$this, 'signalHandler']); 352 | /** @uses $this::signalHandler() */ 353 | pcntl_signal(SIGHUP, [$this, 'signalHandler']); 354 | /** @uses $this::signalHandler() */ 355 | } 356 | 357 | /** 358 | * @param int $signal 359 | */ 360 | private function signalHandler(int $signal) 361 | { 362 | $this->exit++; 363 | 364 | if ($this->exit >= 2) { 365 | throw new SignalException(sprintf('Signal %d', $signal)); 366 | } 367 | } 368 | 369 | /** 370 | * @param InputInterface $input 371 | * @param OutputInterface $output 372 | * @return int 373 | */ 374 | protected function execute(InputInterface $input, OutputInterface $output): int 375 | { 376 | $this->setup($input, $output); 377 | 378 | return $this->getExit(); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/TheFox/FlickrCli/Command/PiwigoCommand.php: -------------------------------------------------------------------------------- 1 | connection; 33 | } 34 | 35 | /** 36 | * @param Connection $connection 37 | */ 38 | public function setConnection(Connection $connection) 39 | { 40 | $this->connection = $connection; 41 | } 42 | 43 | protected function configure() 44 | { 45 | parent::configure(); 46 | 47 | $this->setName('piwigo'); 48 | $this->setDescription('Upload files from Piwigo to Flickr'); 49 | 50 | $this->addOption('piwigo-uploads', null, InputOption::VALUE_REQUIRED, "Path to the Piwigo 'uploads' directory"); 51 | } 52 | 53 | /** 54 | * Executes the current command. 55 | * 56 | * @param InputInterface $input An InputInterface instance 57 | * @param OutputInterface $output An OutputInterface instance 58 | * 59 | * @return int 60 | */ 61 | protected function execute(InputInterface $input, OutputInterface $output): int 62 | { 63 | parent::execute($input, $output); 64 | 65 | $this->checkPiwigoConfig(); 66 | $this->setupPiwigoConnection(); 67 | 68 | $count = $this->getConnection()->query('SELECT COUNT(*) AS count FROM images')->fetchColumn(); 69 | $output->writeln(sprintf('%s images found in the Piwigo database', number_format($count))); 70 | 71 | $piwigoUploadsPath = $input->getOption('piwigo-uploads'); 72 | 73 | // Photos. 74 | $images = $this->getConnection()->query('SELECT * FROM images')->fetchAll(); 75 | foreach ($images as $image) { 76 | $this->processOne($image, $piwigoUploadsPath); 77 | } 78 | 79 | return $this->getExit(); 80 | } 81 | 82 | /** 83 | * @throws RuntimeException 84 | */ 85 | private function checkPiwigoConfig() 86 | { 87 | $config = $this->getConfig(); 88 | 89 | // Piwigo. 90 | if (!isset($config['piwigo']) 91 | || !isset($config['piwigo']['dbname']) 92 | || !isset($config['piwigo']['dbuser']) 93 | || !isset($config['piwigo']['dbpass']) 94 | || !isset($config['piwigo']['dbhost']) 95 | ) { 96 | throw new RuntimeException('Please set the all of the following options in the \'piwigo\' section of config.yml: dbname, dbuser, dbpass, & dbhost.'); 97 | } 98 | } 99 | 100 | private function setupPiwigoConnection() 101 | { 102 | $config = $this->getConfig(); 103 | 104 | $dbConfig = new Configuration(); 105 | $connectionParams = [ 106 | 'dbname' => $config['piwigo']['dbname'], 107 | 'user' => $config['piwigo']['dbuser'], 108 | 'password' => $config['piwigo']['dbpass'], 109 | 'host' => $config['piwigo']['dbhost'], 110 | 'driver' => 'pdo_mysql', 111 | ]; 112 | $conn = DriverManager::getConnection($connectionParams, $dbConfig); 113 | 114 | $this->setConnection($conn); 115 | } 116 | 117 | /** 118 | * @param $image 119 | * @param $piwigoUploadsPath 120 | * @throws RuntimeException 121 | */ 122 | protected function processOne($image, $piwigoUploadsPath) 123 | { 124 | // Check file. 125 | $filePath = $piwigoUploadsPath . substr($image['path'], 9); 126 | if (!file_exists($filePath)) { 127 | throw new RuntimeException(sprintf('File not found: %s', $filePath)); 128 | } 129 | 130 | // Figure out the privacy level. 131 | // 1 = Contacts 132 | // 2 = Friends 133 | // 4 = Family 134 | // 8 = Admins 135 | $isPublic = false; 136 | $isFriend = false; 137 | $isFamily = false; 138 | switch ($image['level']) { 139 | case 0: 140 | $isPublic = true; 141 | break; 142 | case 1: 143 | case 2: 144 | case 4: 145 | $isFriend = true; 146 | $isFamily = true; 147 | break; 148 | case 8: 149 | default: 150 | break; 151 | } 152 | 153 | // Get tags (including a checksum machine tag). 154 | $cats = $this->getConnection()->prepare('SELECT t.name FROM image_tag it JOIN tags t ON it.tag_id=t.id WHERE it.image_id=:id'); 155 | $cats->bindValue('id', $image['id']); 156 | $cats->execute(); 157 | 158 | if (empty($image['md5sum'])) { 159 | $md5sum = md5_file($filePath); 160 | } else { 161 | $md5sum = $image['md5sum']; 162 | } 163 | $tags = [sprintf('checksum:md5=%s', $md5sum)]; 164 | while ($cat = $cats->fetch()) { 165 | $tags[] = $cat['name']; 166 | } 167 | 168 | // Make sure it's not already on Flickr (by MD5 checksum only). 169 | $apiFactory = $this->getApiService()->getApiFactory(); 170 | $md5search = $apiFactory->call('flickr.photos.search', [ 171 | 'user_id' => 'me', 172 | 'tags' => sprintf('checksum:md5=%s', $md5sum), 173 | ]); 174 | if (((int)$md5search->photos['total']) > 0) { 175 | $this->getOutput()->writeln(sprintf('Already exists: %s', $image['name'])); 176 | return; 177 | } 178 | 179 | // Upload to Flickr. 180 | $this->getOutput()->write(sprintf('Uploading: %s', $image['name'])); 181 | $comment = $image['comment']; 182 | $xml = $apiFactory->upload($filePath, $image['name'], $comment, $tags, $isPublic, $isFriend, $isFamily); 183 | $photoId = isset($xml->photoid) ? (int)$xml->photoid : 0; 184 | $stat = isset($xml->attributes()->stat) ? strtolower((string)$xml->attributes()->stat) : ''; 185 | $successful = $stat == 'ok' && $photoId != 0; 186 | if (!$successful) { 187 | throw new RuntimeException(sprintf('Failed to upload %s to %s', $filePath, $image['name'])); 188 | } 189 | 190 | // Add to albums (categories, in Piwigo parlance). 191 | $this->getOutput()->write(' [photosets]'); 192 | $sql = 'SELECT c.name FROM image_category ic JOIN categories c ON ic.category_id=c.id WHERE ic.image_id=:id'; 193 | $cats = $this->getConnection()->prepare($sql); 194 | $cats->bindValue('id', $image['id']); 195 | $cats->execute(); 196 | while ($cat = $cats->fetch()) { 197 | $photosetId = $this->getPhotosetId($cat['name'], $photoId); 198 | $apiFactory->call('flickr.photosets.addPhoto', [ 199 | 'photoset_id' => $photosetId, 200 | 'photo_id' => $photoId, 201 | ]); 202 | } 203 | 204 | // Add to an import photoset. 205 | $importFromPiwigoId = $this->getPhotosetId('Imported from Piwigo', $photoId); 206 | $apiFactory->call('flickr.photosets.addPhoto', [ 207 | 'photoset_id' => $importFromPiwigoId, 208 | 'photo_id' => $photoId, 209 | ]); 210 | 211 | // Set location on Flickr. 212 | if (!empty($image['latitude']) && !empty($image['longitude'])) { 213 | $this->getOutput()->write(' [location]'); 214 | $apiFactory->call('flickr.photos.geo.setLocation', [ 215 | 'photo_id' => $photoId, 216 | 'lat' => $image['latitude'], 217 | 'lon' => $image['longitude'], 218 | ]); 219 | } else { 220 | $this->getOutput()->write(' [no location]'); 221 | } 222 | 223 | $this->getOutput()->writeln(' -- done'); 224 | } 225 | 226 | /** 227 | * Get a photoset's ID from a name, creating a new photo set if required. 228 | * Case insensitive. 229 | * 230 | * @param string $photosetName 231 | * @param int $primaryPhotoId 232 | * @return int 233 | */ 234 | protected function getPhotosetId($photosetName, $primaryPhotoId) 235 | { 236 | $apiFactory = $this->getApiService()->getApiFactory(); 237 | 238 | // First get all existing albums (once only). 239 | if (!is_array($this->photosets)) { 240 | $this->photosets = []; 241 | $getList = $apiFactory->call('flickr.photosets.getList'); 242 | /** 243 | * @var string $n 244 | * @var \SimpleXMLElement $photoset 245 | */ 246 | foreach ($getList->photosets->photoset as $n => $photoset) { 247 | $this->photosets[(int)$photoset->attributes()->id] = (string)$photoset->title; 248 | } 249 | } 250 | 251 | // See if we've already got it. 252 | foreach ($this->photosets as $id => $name) { 253 | if (mb_strtolower($photosetName) != mb_strtolower($name)) { 254 | continue; 255 | } 256 | 257 | return (int)$id; 258 | } 259 | 260 | // Otherwise, create it. 261 | $this->getOutput()->write(sprintf(' [creating new photoset: %s]', $photosetName)); 262 | $newPhotoset = $apiFactory->call('flickr.photosets.create', [ 263 | 'title' => $photosetName, 264 | 'primary_photo_id' => $primaryPhotoId, 265 | ]); 266 | $newId = (int)$newPhotoset->photoset->attributes()->id; 267 | $this->photosets[$newId] = $photosetName; 268 | return $newId; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/TheFox/FlickrCli/Command/UploadCommand.php: -------------------------------------------------------------------------------- 1 | setName('upload'); 27 | $this->setDescription('Upload files to Flickr.'); 28 | 29 | $this->addOption('description', 'd', InputOption::VALUE_OPTIONAL, 'Description for all uploaded files.'); 30 | 31 | $csvTagsDesc = 'Comma separated names. For example: --tags=tag1,"Tag two"'; 32 | $this->addOption('tags', 't', InputOption::VALUE_OPTIONAL, $csvTagsDesc); 33 | 34 | $csvSetsDesc = 'Comma separated names. For example: --sets="Set one",set2'; 35 | $this->addOption('sets', 's', InputOption::VALUE_OPTIONAL, $csvSetsDesc); 36 | 37 | $this->addOption('recursive', 'r', InputOption::VALUE_NONE, 'Recurse into directories.'); 38 | $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would have been transferred.'); 39 | $this->addOption('move', 'm', InputOption::VALUE_OPTIONAL, 'Move uploaded files to this directory.'); 40 | 41 | $this->addArgument('directory', InputArgument::IS_ARRAY, 'Path to directories.'); 42 | } 43 | 44 | /** 45 | * @param InputInterface $input 46 | * @param OutputInterface $output 47 | * @return int 48 | */ 49 | protected function execute(InputInterface $input, OutputInterface $output): int 50 | { 51 | parent::execute($input, $output); 52 | 53 | $apiService = $this->getApiService(); 54 | 55 | if ($input->hasOption('description') && $input->getOption('description')) { 56 | $description = $input->getOption('description'); 57 | $this->getLogger()->info(sprintf('Description: %s', $description)); 58 | } else { 59 | $description = null; 60 | } 61 | 62 | if ($input->hasOption('tags') && $input->getOption('tags')) { 63 | $tags = $input->getOption('tags'); 64 | $this->getLogger()->debug(sprintf('Tags String: %s', $tags)); 65 | } else { 66 | $tags = null; 67 | } 68 | 69 | $recursive = $input->getOption('recursive'); 70 | $dryrun = $input->getOption('dry-run'); 71 | 72 | //$metadata = new Metadata($config['flickr']['consumer_key'], $config['flickr']['consumer_secret']); 73 | //$metadata->setOauthAccess($config['flickr']['token'], $config['flickr']['token_secret']); 74 | 75 | //$guzzleAdapter = new RezzzaGuzzleAdapter(); 76 | $guzzleAdapterVerbose = new RezzzaGuzzleAdapter(); 77 | $guzzleAdapterClient = $guzzleAdapterVerbose->getClient(); 78 | $guzzleAdapterClientConfig = $guzzleAdapterClient->getConfig(); 79 | 80 | $curlOptions = $guzzleAdapterClientConfig->get(GuzzleHttpClient::CURL_OPTIONS); 81 | $curlOptions[CURLOPT_CONNECTTIMEOUT] = 60; 82 | $curlOptions[CURLOPT_NOPROGRESS] = false; 83 | 84 | $timePrev = 0; 85 | $uploadedTotal = 0; 86 | $uploadedPrev = 0; 87 | $uploadedDiffPrev = [0, 0, 0, 0, 0]; 88 | 89 | $curlOptions[CURLOPT_PROGRESSFUNCTION] = function ($ch, $dlTotal = 0, $dlNow = 0, $ulTotal = 0, $ulNow = 0) use ($timePrev, $uploadedTotal, $uploadedPrev, $uploadedDiffPrev) { 90 | 91 | $uploadedDiff = $ulNow - $uploadedPrev; 92 | $uploadedPrev = $ulNow; 93 | $uploadedTotal += $uploadedDiff; 94 | 95 | $percent = 0; 96 | if ($ulTotal) { 97 | $percent = $ulNow / $ulTotal * 100; 98 | } 99 | if ($percent > 100) { 100 | $percent = 100; 101 | } 102 | 103 | $progressbarUploaded = round($percent / 100 * FlickrCli::UPLOAD_PROGRESSBAR_ITEMS); 104 | $progressbarRest = FlickrCli::UPLOAD_PROGRESSBAR_ITEMS - $progressbarUploaded; 105 | 106 | $uploadedDiffStr = ''; 107 | $timeCur = time(); 108 | if ($timeCur != $timePrev) { 109 | //$timePrev = $timeCur; 110 | 111 | $uploadedDiff = ($uploadedDiff + array_sum($uploadedDiffPrev)) / 6; 112 | array_shift($uploadedDiffPrev); 113 | $uploadedDiffPrev[] = $uploadedDiff; 114 | 115 | if ($uploadedDiff > 0) { 116 | $bytesize = new ByteSize(); 117 | $uploadedDiffStr = $bytesize->format($uploadedDiff) . '/s'; 118 | } 119 | } 120 | 121 | printf( 122 | "[file] %6.2f%% [%s%s] %s %10s\x1b[0K\r", 123 | $percent, 124 | str_repeat('#', $progressbarUploaded), 125 | str_repeat(' ', $progressbarRest), 126 | number_format($ulNow), 127 | $uploadedDiffStr 128 | ); 129 | 130 | pcntl_signal_dispatch(); 131 | 132 | return $this->getExit() >= 2 ? 1 : 0; 133 | }; 134 | $guzzleAdapterClientConfig->set(GuzzleHttpClient::CURL_OPTIONS, $curlOptions); 135 | 136 | //$apiFactory = new ApiFactory($metadata, $guzzleAdapter); 137 | $apiFactory = $apiService->getApiFactory(); 138 | $metadata = $apiFactory->getMetadata(); 139 | $apiFactoryVerbose = new ApiFactory($metadata, $guzzleAdapterVerbose); 140 | 141 | if ($input->getOption('sets')) { 142 | $photosetNames = preg_split('/,/', $input->getOption('sets')); 143 | } else { 144 | $photosetNames = []; 145 | } 146 | 147 | $photosetAll = []; 148 | $photosetAllLower = []; 149 | 150 | $apiFactory = $apiService->getApiFactory(); 151 | $xml = $apiFactory->call('flickr.photosets.getList'); 152 | 153 | /** 154 | * @var int $n 155 | * @var SimpleXMLElement $photoset 156 | */ 157 | foreach ($xml->photosets->photoset as $n => $photoset) { 158 | pcntl_signal_dispatch(); 159 | if ($this->getExit()) { 160 | break; 161 | } 162 | 163 | $id = (int)$photoset->attributes()->id; 164 | $title = (string)$photoset->title; 165 | 166 | $photosetAll[$id] = $title; 167 | $photosetAllLower[$id] = strtolower($title); 168 | } 169 | 170 | $photosets = []; 171 | $photosetsNew = []; 172 | foreach ($photosetNames as $photosetTitle) { 173 | $id = 0; 174 | 175 | foreach ($photosetAllLower as $photosetAllId => $photosetAllTitle) { 176 | if (strtolower($photosetTitle) != $photosetAllTitle) { 177 | continue; 178 | } 179 | 180 | $id = $photosetAllId; 181 | break; 182 | } 183 | if ($id) { 184 | $photosets[] = $id; 185 | } else { 186 | $photosetsNew[] = $photosetTitle; 187 | } 188 | } 189 | 190 | // Move files after they've been successfully uploaded? 191 | $configUploadedBaseDir = false; 192 | $move = $input->getOption('move'); 193 | if (null !== $move) { 194 | $configUploadedBaseDir = dirname($move); 195 | 196 | $filesystem = new Filesystem(); 197 | // Make the local directory if it doesn't exist. 198 | if (!$filesystem->exists($configUploadedBaseDir)) { 199 | $filesystem->mkdir($configUploadedBaseDir, 0755); 200 | $this->getLogger()->info(sprintf('Created directory: %s', $configUploadedBaseDir)); 201 | } 202 | $this->getLogger()->info(sprintf('Uploaded files will be moved to: %s', $configUploadedBaseDir)); 203 | } 204 | 205 | $totalFiles = 0; 206 | $totalFilesUploaded = 0; 207 | $fileErrors = 0; 208 | $filesFailed = []; 209 | 210 | $filter = function (SplFileInfo $file) { 211 | if (in_array($file->getFilename(), FlickrCli::FILES_INORE)) { 212 | return false; 213 | } 214 | return true; 215 | }; 216 | $finder = new Finder(); 217 | $finder->files()->filter($filter); 218 | if (!$recursive) { 219 | $finder->depth(0); 220 | } 221 | 222 | $bytesize = new ByteSize(); 223 | 224 | $directories = $input->getArgument('directory'); 225 | foreach ($directories as $argDir) { 226 | if ($configUploadedBaseDir) { 227 | $argDirReplaced = str_replace('/', '_', $argDir); 228 | $uploadBaseDirPath = sprintf('%s/%s', $configUploadedBaseDir, $argDirReplaced); 229 | } else { 230 | $uploadBaseDirPath = ''; 231 | } 232 | 233 | $this->getLogger()->info(sprintf('[dir] upload dir: %s %s', $argDir, $uploadBaseDirPath)); 234 | 235 | /** @var \Symfony\Component\Finder\SplFileInfo[] $files */ 236 | $files = iterator_to_array($finder->in($argDir)); 237 | sort($files); 238 | foreach ($files as $file) { 239 | pcntl_signal_dispatch(); 240 | if ($this->getExit()) { 241 | break; 242 | } 243 | 244 | $fileName = $file->getFilename(); 245 | $fileExt = $file->getExtension(); 246 | $filePath = $file->getRealPath(); 247 | $fileRelativePath = new SplFileInfo($file->getRelativePathname()); 248 | $fileRelativePathStr = (string)$fileRelativePath; 249 | $dirRelativePath = $fileRelativePath->getPath(); 250 | 251 | $uploadFileSize = filesize($filePath); 252 | //$uploadFileSizeLen = strlen(number_format($uploadFileSize)); 253 | $uploadFileSizeFormatted = $bytesize->format($uploadFileSize); 254 | 255 | $uploadDirPath = ''; 256 | if ($uploadBaseDirPath) { 257 | $uploadDirPath = sprintf('%s/%s', $uploadBaseDirPath, $dirRelativePath); 258 | 259 | $filesystem = new Filesystem(); 260 | if (!$filesystem->exists($uploadDirPath)) { 261 | $this->getLogger()->info(sprintf('[dir] create "%s"', $uploadDirPath)); 262 | $filesystem->mkdir($uploadDirPath); 263 | } 264 | } 265 | 266 | $totalFiles++; 267 | 268 | if (!in_array(strtolower($fileExt), FlickrCli::ACCEPTED_EXTENTIONS)) { 269 | $fileErrors++; 270 | $filesFailed[] = $fileRelativePathStr; 271 | $this->getLogger()->warning(sprintf('[file] invalid extension: %s', $fileRelativePathStr)); 272 | 273 | continue; 274 | } 275 | 276 | if ($dryrun) { 277 | $this->getLogger()->info(sprintf( 278 | "[file] dry upload '%s' '%s' %s", 279 | $fileRelativePathStr, 280 | $dirRelativePath, 281 | $uploadFileSizeFormatted 282 | )) 283 | ; 284 | continue; 285 | } 286 | 287 | $this->getLogger()->info(sprintf('[file] upload "%s" %s', $fileRelativePathStr, $uploadFileSizeFormatted)); 288 | try { 289 | $xml = $apiFactoryVerbose->upload($filePath, $fileName, $description, $tags); 290 | 291 | print "\n"; 292 | } catch (Exception $e) { 293 | $this->getLogger()->error(sprintf('[file] upload: %s', $e->getMessage())); 294 | $xml = null; 295 | } 296 | 297 | if ($xml) { 298 | $photoId = isset($xml->photoid) ? (int)$xml->photoid : 0; 299 | $stat = isset($xml->attributes()->stat) ? strtolower((string)$xml->attributes()->stat) : ''; 300 | $successful = $stat == 'ok' && $photoId != 0; 301 | } else { 302 | $photoId = 0; 303 | $successful = false; 304 | } 305 | 306 | if ($successful) { 307 | $logLine = 'OK'; 308 | $totalFilesUploaded++; 309 | 310 | if ($uploadDirPath) { 311 | $this->getLogger()->info(sprintf('[file] move to uploaded dir: %s', $uploadDirPath)); 312 | 313 | $filesystem = new Filesystem(); 314 | $filesystem->rename($filePath, sprintf('%s/%s', $uploadDirPath, $fileName)); 315 | } 316 | } else { 317 | $logLine = 'FAILED'; 318 | $fileErrors++; 319 | $filesFailed[] = $fileRelativePathStr; 320 | } 321 | $this->getLogger()->info(sprintf('[file] status: %s - ID %s', $logLine, $photoId)); 322 | 323 | if (!$successful) { 324 | continue; 325 | } 326 | 327 | if ($photosetsNew) { 328 | foreach ($photosetsNew as $photosetTitle) { 329 | $this->getLogger()->info(sprintf('[photoset] create %s ... ', $photosetTitle)); 330 | 331 | $xml = null; 332 | try { 333 | $xml = $apiFactory->call('flickr.photosets.create', [ 334 | 'title' => $photosetTitle, 335 | 'primary_photo_id' => $photoId, 336 | ]); 337 | } catch (Exception $e) { 338 | $this->getLogger()->critical(sprintf('[photoset] create %s FAILED: %s', $photosetTitle, $e->getMessage())); 339 | return 1; 340 | } 341 | 342 | if ((string)$xml->attributes()->stat == 'ok') { 343 | $photosetId = (int)$xml->photoset->attributes()->id; 344 | $photosets[] = $photosetId; 345 | 346 | $this->getLogger()->info(sprintf('[photoset] create %s OK - ID %s', $photosetTitle, $photosetId)); 347 | } else { 348 | $code = (int)$xml->err->attributes()->code; 349 | $this->getLogger()->critical(sprintf('[photoset] create %s FAILED: %s', $photosetTitle, $code)); 350 | return 1; 351 | } 352 | } 353 | $photosetsNew = null; 354 | } 355 | 356 | if (count($photosets)) { 357 | $this->getLogger()->info('[file] add to sets ... '); 358 | 359 | $logLine = []; 360 | foreach ($photosets as $photosetId) { 361 | $logLine[] = substr($photosetId, -5); 362 | 363 | try { 364 | $xml = $apiFactory->call('flickr.photosets.addPhoto', [ 365 | 'photoset_id' => $photosetId, 366 | 'photo_id' => $photoId, 367 | ]); 368 | } catch (Exception $e) { 369 | $this->getLogger()->critical(sprintf('[file] add to sets FAILED: %s', $e->getMessage())); 370 | return 1; 371 | } 372 | 373 | if ($xml->attributes()->stat == 'ok') { 374 | $logLine[] = 'OK'; 375 | } else { 376 | if (isset($xml->err)) { 377 | $code = (int)$xml->err->attributes()->code; 378 | if ($code == 3) { 379 | $logLine[] = 'OK'; 380 | } else { 381 | $this->getLogger()->critical(sprintf('[file] add to sets FAILED: %d', $code)); 382 | return 1; 383 | } 384 | } else { 385 | $this->getLogger()->critical('[file] add to sets FAILED'); 386 | return 1; 387 | } 388 | } 389 | } 390 | 391 | $this->getLogger()->info(sprintf('[file] added to sets: %s', join(' ', $logLine))); 392 | } 393 | } 394 | } 395 | 396 | if ($uploadedTotal > 0) { 397 | $uploadedTotalStr = $bytesize->format($uploadedTotal); 398 | } else { 399 | $uploadedTotalStr = 0; 400 | } 401 | 402 | $this->getLogger()->notice(sprintf('[main] total uploaded: %s', $uploadedTotalStr)); 403 | $this->getLogger()->notice(sprintf('[main] total files: %d', $totalFiles)); 404 | $this->getLogger()->notice(sprintf('[main] files uploaded: %d', $totalFilesUploaded)); 405 | 406 | $filesFailedMsg = count($filesFailed) ? "\n" . join("\n", $filesFailed) : ''; 407 | $this->getLogger()->notice(sprintf('[main] files failed: %s%s', $fileErrors, $filesFailedMsg)); 408 | 409 | return $this->getExit(); 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /src/TheFox/FlickrCli/Exception/SignalException.php: -------------------------------------------------------------------------------- 1 | logger; 17 | } 18 | 19 | /** 20 | * @param LoggerInterface $logger 21 | */ 22 | public function setLogger(LoggerInterface $logger) 23 | { 24 | $this->logger = $logger; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/TheFox/FlickrCli/Service/ApiService.php: -------------------------------------------------------------------------------- 1 | setLogger(new NullLogger()); 44 | 45 | $this->consumerKey = $consumerKey; 46 | $this->consumerSecret = $consumerSecret; 47 | $this->token = $token; 48 | $this->tokenSecret = $tokenSecret; 49 | } 50 | 51 | /** 52 | * @return ApiFactory 53 | */ 54 | public function getApiFactory(): ApiFactory 55 | { 56 | // Set up the Flickr API. 57 | $metadata = new Metadata($this->consumerKey, $this->consumerSecret); 58 | $metadata->setOauthAccess($this->token, $this->tokenSecret); 59 | $adapter = new RezzzaGuzzleAdapter(); 60 | $apiFactory = new ApiFactory($metadata, $adapter); 61 | 62 | return $apiFactory; 63 | } 64 | 65 | /** 66 | * @return array 67 | */ 68 | public function getPhotosetTitles(): array 69 | { 70 | $apiFactory = $this->getApiFactory(); 71 | 72 | $this->getLogger()->info('[main] get photosets'); 73 | $xml = $apiFactory->call('flickr.photosets.getList'); 74 | 75 | /** 76 | * @var int $n 77 | * @var SimpleXMLElement $photoset 78 | */ 79 | foreach ($xml->photosets->photoset as $n => $photoset) { 80 | $id = (int)$photoset->attributes()->id; 81 | $photosetsTitles[$id] = (string)$photoset->title; 82 | } 83 | 84 | asort($photosetsTitles); 85 | 86 | return $photosetsTitles; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/TheFox/OAuth/Common/Http/Client/GuzzleStreamClient.php: -------------------------------------------------------------------------------- 1 | 'close']; 32 | $headers = array_merge($headers, $extraHeaders); 33 | $response = null; 34 | 35 | if ($method == 'POST') { 36 | $request = $client->post($endpoint->getAbsoluteUri(), $headers, $requestBody); 37 | $response = $request->send(); 38 | } elseif ($method == 'GET') { 39 | throw new InvalidArgumentException('"GET" request not implemented.'); 40 | } 41 | 42 | if ($response && !$response->isSuccessful()) { 43 | throw new TokenResponseException('Failed to request token.'); 44 | } 45 | 46 | $responseHtml = (string)$response->getBody(); 47 | return $responseHtml; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/TheFox/OAuth/OAuth1/Service/Flickr.php: -------------------------------------------------------------------------------- 1 | baseApiUri = new Uri('https://api.flickr.com/services/rest/?'); 37 | } 38 | } 39 | 40 | /** 41 | * @return Uri 42 | */ 43 | public function getRequestTokenEndpoint(): Uri 44 | { 45 | return new Uri('https://www.flickr.com/services/oauth/request_token'); 46 | } 47 | 48 | /** 49 | * @return Uri 50 | */ 51 | public function getAuthorizationEndpoint(): Uri 52 | { 53 | return new Uri('https://www.flickr.com/services/oauth/authorize'); 54 | } 55 | 56 | /** 57 | * @return Uri 58 | */ 59 | public function getAccessTokenEndpoint(): Uri 60 | { 61 | return new Uri('https://www.flickr.com/services/oauth/access_token'); 62 | } 63 | 64 | /** 65 | * @param string $responseBody 66 | * @return StdOAuth1Token 67 | * @throws TokenResponseException 68 | */ 69 | protected function parseRequestTokenResponse($responseBody): StdOAuth1Token 70 | { 71 | parse_str($responseBody, $data); 72 | if (null === $data || !is_array($data)) { 73 | throw new TokenResponseException('Unable to parse response.'); 74 | } elseif (!isset($data['oauth_callback_confirmed']) || $data['oauth_callback_confirmed'] != 'true') { 75 | throw new TokenResponseException('Error in retrieving token.'); 76 | } 77 | return $this->parseAccessTokenResponse($responseBody); 78 | } 79 | 80 | /** 81 | * @param string $responseBody 82 | * @return StdOAuth1Token 83 | * @throws TokenResponseException 84 | */ 85 | protected function parseAccessTokenResponse($responseBody): StdOAuth1Token 86 | { 87 | #print "parseAccessTokenResponse\n"; 88 | 89 | parse_str($responseBody, $data); 90 | if ($data === null || !is_array($data)) { 91 | throw new TokenResponseException('Unable to parse response.'); 92 | } elseif (isset($data['error'])) { 93 | throw new TokenResponseException('Error in retrieving token: "' . $data['error'] . '"'); 94 | } 95 | 96 | $token = new StdOAuth1Token(); 97 | $token->setRequestToken($data['oauth_token']); 98 | $token->setRequestTokenSecret($data['oauth_token_secret']); 99 | $token->setAccessToken($data['oauth_token']); 100 | $token->setAccessTokenSecret($data['oauth_token_secret']); 101 | $token->setEndOfLife(StdOAuth1Token::EOL_NEVER_EXPIRES); 102 | unset($data['oauth_token'], $data['oauth_token_secret']); 103 | $token->setExtraParams($data); 104 | 105 | return $token; 106 | } 107 | } 108 | --------------------------------------------------------------------------------