├── .env.example ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── linters │ ├── .markdownlint.yml │ └── .yaml-lint.yml ├── scripts │ └── get-path-prefix.js ├── super-linter.env └── workflows │ ├── algolia-indexing.yml │ ├── deploy.yml │ ├── github-pages.yml │ └── test-pull-request.yml ├── .gitignore ├── .yarn └── releases │ └── yarn-3.2.2.cjs ├── .yarnrc.yml ├── CODE_OF_CONDUCT.md ├── COPYRIGHT ├── LICENSE ├── README.md ├── gatsby-config.js ├── gatsby-ssr.js ├── package.json ├── samples ├── LICENSE ├── README.md ├── doc.md ├── partner_days_2021 │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── server │ │ └── index.js │ └── views │ │ └── index.jade └── src │ ├── common │ ├── crypto │ │ └── CryptoUtils.mjs │ ├── lr │ │ ├── LrAuth.mjs │ │ ├── LrContext.mjs │ │ ├── LrRequestor.mjs │ │ ├── LrSession.mjs │ │ └── LrUtils.mjs │ ├── original │ │ └── OriginalUtils.mjs │ └── request │ │ └── RequestUtils.mjs │ ├── node │ ├── .gitignore │ ├── README.md │ ├── common │ │ ├── file │ │ │ ├── Directory.mjs │ │ │ ├── File.mjs │ │ │ └── FileUtils.mjs │ │ └── proxy │ │ │ └── delay │ │ │ └── ProxyDelayHTTP.js │ ├── connect │ │ ├── connectdir.mjs │ │ ├── createproject.mjs │ │ ├── deletealbum.mjs │ │ ├── getprojects.mjs │ │ └── logconnector.mjs │ ├── err │ │ ├── abort.mjs │ │ └── apikey.mjs │ ├── gen │ │ ├── README.md │ │ ├── gen2560.mjs │ │ ├── genfullsize.mjs │ │ ├── getasset2560.mjs │ │ ├── getassetfullsize.mjs │ │ ├── renditionsexist.mjs │ │ ├── waitfor2560.mjs │ │ └── waitforfullsize.mjs │ ├── get │ │ ├── getalbum.mjs │ │ ├── getalbumassets.mjs │ │ ├── getalbumcover.mjs │ │ ├── getasset.mjs │ │ ├── getasset2048.mjs │ │ ├── getassetthumb.mjs │ │ ├── getcollections.mjs │ │ ├── getfirstalbumasset.mjs │ │ ├── getfirstasset.mjs │ │ └── getincompletes.mjs │ ├── health │ │ └── gethealth.mjs │ └── upload │ │ └── uploadfile.mjs │ └── web │ ├── .gitignore │ ├── README.md │ ├── aborttest │ ├── index.html │ └── index.js │ ├── browsecolumn │ ├── index.html │ ├── index.js │ └── styles.css │ ├── common │ ├── file │ │ ├── File.js │ │ ├── FileDeferred.js │ │ └── FileUtils.js │ ├── grid │ │ └── LrGrid.js │ ├── image │ │ ├── LrAssetRenditionProvider.js │ │ └── LrAssetThumbnailManager.js │ └── info │ │ ├── InfoView.css │ │ └── InfoView.js │ ├── package-lock.json │ ├── package.json │ ├── pickasset │ ├── index.html │ ├── index.js │ └── styles.css │ ├── upload │ ├── index.html │ ├── index.js │ └── styles.css │ └── userinfo │ ├── index.html │ ├── index.js │ └── styles.css ├── src └── pages │ ├── api │ └── index.md │ ├── code-sample │ └── index.md │ ├── getting-started │ ├── authenticate_customers │ │ └── index.md │ ├── branding_guidelines │ │ └── index.md │ ├── create_integration │ │ └── index.md │ ├── developer_guidelines │ │ └── index.md │ ├── index.md │ ├── manage_content │ │ └── index.md │ ├── read_generate_renditions │ │ └── index.md │ ├── service_health_check │ │ └── index.md │ └── upload_content │ │ └── index.md │ ├── guides │ ├── calling_api │ │ └── index.md │ ├── common_data_model │ │ └── index.md │ ├── index.md │ ├── links_and_pagination │ │ └── index.md │ └── primitive_data_types │ │ └── index.md │ ├── hero.png │ ├── index.md │ ├── release-notes │ └── index.md │ └── support │ ├── FAQ │ └── index.md │ ├── community │ └── index.md │ ├── contribute │ └── index.md │ ├── experience_cloud.png │ ├── index.md │ └── stack-overflow.png ├── static ├── AutoUploadFromPartnerAppToLR.png ├── BulkUploadOfSelectedAssetsFromPartnerAppToLR.png ├── ConnectWorkflowDiagrams.png ├── IMS_Authorization_flow.png ├── OAuthFlowDiagram.png ├── PrintWorkflow.png ├── UserLoggedInFromPartnerAppToLR.png └── swagger.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | REPO_GITHUB_TOKEN= 2 | REPO_OWNER=AdobeDocs 3 | REPO_NAME=dev-site-documentation-template 4 | REPO_BRANCH=main 5 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Adobe Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language. 18 | * Being respectful of differing viewpoints and experiences. 19 | * Gracefully accepting constructive criticism. 20 | * Focusing on what is best for the community. 21 | * Showing empathy towards other community members. 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances. 27 | * Trolling, insulting/derogatory comments, and personal or political attacks. 28 | * Public or private harassment. 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission. 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting. 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Grp-opensourceoffice@adobe.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version]. 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for choosing to contribute! 4 | 5 | The following are a set of guidelines to follow when contributing to this project. 6 | 7 | ## Code Of Conduct 8 | 9 | This project adheres to the Adobe [code of conduct](CODE_OF_CONDUCT.md). By participating, 10 | you are expected to uphold this code. Please report unacceptable behavior to 11 | [Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com). 12 | 13 | ## Have A Question? 14 | 15 | Start by filing an issue. The existing committers on this project work to reach 16 | consensus around project direction and issue solutions within issue threads 17 | (when appropriate). 18 | 19 | ## Contributor License Agreement 20 | 21 | All third-party contributions to this project must be accompanied by a signed contributor 22 | license agreement. This gives Adobe permission to redistribute your contributions 23 | as part of the project. [Sign our CLA](https://opensource.adobe.com/cla.html). You 24 | only need to submit an Adobe CLA one time, so if you have submitted one previously, 25 | you are good to go! 26 | 27 | ## Code Reviews 28 | 29 | All submissions should come in the form of pull requests and need to be reviewed 30 | by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/) 31 | for more information on sending pull requests. 32 | 33 | Lastly, please follow the [pull request template](PULL_REQUEST_TEMPLATE.md) when 34 | submitting a pull request! 35 | 36 | ## From Contributor To Committer 37 | 38 | We love contributions from our community! If you'd like to go a step beyond contributor 39 | and become a committer with full write access and a say in the project, you must 40 | be invited to the project. The existing committers employ an internal nomination 41 | process that must reach lazy consensus (silence is approval) before invitations 42 | are issued. If you feel you are qualified and want to get more deeply involved, 43 | feel free to reach out to existing committers to have a conversation about that. 44 | 45 | ## Security Issues 46 | 47 | Security issues shouldn't be reported on this issue tracker. Instead, [file an issue to our security experts](https://helpx.adobe.com/security/alertus.html). 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ### Expected Behaviour 5 | 6 | ### Actual Behaviour 7 | 8 | ### Reproduce Scenario (including but not limited to) 9 | 10 | #### Steps to Reproduce 11 | 12 | #### Platform and Version 13 | 14 | #### Sample Code that illustrates the problem 15 | 16 | #### Logs taken while reproducing problem 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Related Issue 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Motivation and Context 15 | 16 | 17 | 18 | ## How Has This Been Tested? 19 | 20 | 21 | 22 | 23 | 24 | ## Screenshots (if appropriate): 25 | 26 | ## Types of changes 27 | 28 | 29 | 30 | - [ ] Bug fix (non-breaking change which fixes an issue) 31 | - [ ] New feature (non-breaking change which adds functionality) 32 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 33 | 34 | ## Checklist: 35 | 36 | 37 | 38 | 39 | - [ ] I have signed the [Adobe Open Source CLA](https://opensource.adobe.com/cla.html). 40 | - [ ] My code follows the code style of this project. 41 | - [ ] My change requires a change to the documentation. 42 | - [ ] I have updated the documentation accordingly. 43 | - [ ] I have read the **CONTRIBUTING** document. 44 | - [ ] I have added tests to cover my changes. 45 | - [ ] All new and existing tests passed. 46 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Enable version updates for npm 4 | - package-ecosystem: "npm" 5 | # Look for `package.json` and `lock` files in the `root` directory 6 | directory: "/" 7 | # Check the npm registry for updates every day (weekdays) 8 | schedule: 9 | interval: "daily" 10 | allow: 11 | - dependency-name: "@adobe/gatsby-theme-aio" 12 | versioning-strategy: increase 13 | open-pull-requests-limit: 25 14 | labels: 15 | - "dependencies" 16 | ignore: 17 | # Ignore updates to package 18 | - dependency-name: "gatsby" 19 | -------------------------------------------------------------------------------- /.github/linters/.markdownlint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ########################### 3 | ########################### 4 | ## Markdown Linter rules ## 5 | ########################### 6 | ########################### 7 | 8 | # Linter rules doc: 9 | # - https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md 10 | # 11 | # Note: 12 | # To comment out a single error: 13 | # 14 | # any violations you want 15 | # 16 | # 17 | 18 | # Default state for all rules 19 | default: false 20 | 21 | ################# 22 | # Rules by tags # 23 | ################# 24 | blanks-around-fences: true # Fenced code blocks should be surrounded by blank lines 25 | blanks-around-headings: true # Headings should be surrounded by blank lines 26 | blanks-around-lists: true # Lists should be surrounded by blank lines 27 | code-block-style: 28 | style: "fenced" 29 | code-fence-style: 30 | style: "backtick" 31 | emphasis-style: 32 | style: "consistent" 33 | fenced-code-language: true # Fenced code blocks should have a language specified 34 | heading-start-left: true # Headings must start at the beginning of the line 35 | heading-style: 36 | style: "atx" 37 | hr-style: true # Horizontal rule style 38 | list-indent: true # Inconsistent indentation for list items at the same level 39 | no-empty-links: true 40 | no-missing-space-atx: true # No space after hash on atx style heading 41 | no-multiple-blanks: true # Multiple consecutive blank lines 42 | no-reversed-links: true 43 | no-space-in-code: true 44 | no-space-in-emphasis: true 45 | no-space-in-links: true 46 | no-trailing-spaces: true 47 | single-trailing-newline: true # Files should end with a single newline character 48 | strong-style: 49 | style: "consistent" 50 | ul-style: 51 | style: "consistent" 52 | -------------------------------------------------------------------------------- /.github/linters/.yaml-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ########################################### 3 | # These are the rules used for # 4 | # linting all the yaml files in the stack # 5 | # NOTE: # 6 | # You can disable line with: # 7 | # # yamllint disable-line # 8 | ########################################### 9 | rules: 10 | braces: 11 | level: warning 12 | min-spaces-inside: 0 13 | max-spaces-inside: 0 14 | min-spaces-inside-empty: 1 15 | max-spaces-inside-empty: 5 16 | brackets: 17 | level: warning 18 | min-spaces-inside: 0 19 | max-spaces-inside: 0 20 | min-spaces-inside-empty: 1 21 | max-spaces-inside-empty: 5 22 | colons: 23 | level: warning 24 | max-spaces-before: 0 25 | max-spaces-after: 1 26 | commas: 27 | level: warning 28 | max-spaces-before: 0 29 | min-spaces-after: 1 30 | max-spaces-after: 1 31 | comments: disable 32 | comments-indentation: disable 33 | document-end: disable 34 | document-start: 35 | level: warning 36 | present: true 37 | empty-lines: 38 | level: warning 39 | max: 2 40 | max-start: 0 41 | max-end: 0 42 | hyphens: 43 | level: warning 44 | max-spaces-after: 1 45 | indentation: 46 | level: warning 47 | spaces: consistent 48 | indent-sequences: true 49 | check-multi-line-strings: false 50 | key-duplicates: enable 51 | line-length: 52 | level: warning 53 | max: 100 54 | allow-non-breakable-words: true 55 | allow-non-breakable-inline-mappings: true 56 | new-line-at-end-of-file: disable 57 | new-lines: 58 | type: unix 59 | trailing-spaces: disable 60 | -------------------------------------------------------------------------------- /.github/scripts/get-path-prefix.js: -------------------------------------------------------------------------------- 1 | // This script retrieves the pathPrefix from the gatsby-config.js file. 2 | // It serves as an example for how to set up external javascript functions 3 | // outside workflow .yml files when they get too big or complex to keep them inline. 4 | 5 | // Documentation for the actions/github-script: 6 | // https://github.com/actions/github-script#run-a-separate-file 7 | 8 | module.exports = async ({ core }) => { 9 | const { pathPrefix } = await require('../../gatsby-config.js'); 10 | 11 | if (!pathPrefix) { 12 | core.setFailed( 13 | `The pathPrefix in the site's gatsby-config.js file is missing. 14 | 15 | To fix this, open your gatsby-config.js file, and add it to the config object: 16 | 17 | module.exports = { 18 | pathPrefix: "/commerce/frontend-core/", 19 | ... 20 | }` 21 | ); 22 | } else if (pathPrefix === '/') { 23 | core.setFailed( 24 | `The pathPrefix in the site's gatsby-config.js file is set to "/". This is not allowed. 25 | 26 | To fix this, change the pathPrefix to include a name that starts and ends with "/": 27 | 28 | pathPrefix: "/commerce/frontend - core/" 29 | 30 | This name identifies the site within the developer.adobe.com domain: 31 | https://developer.adobe.com/document-services/. 32 | ` 33 | ); 34 | } else { 35 | if (!pathPrefix.startsWith('/') || !pathPrefix.endsWith('/')) { 36 | core.setFailed( 37 | `The pathPrefix in the site's gatsby-config.js file does not start or end with "/". 38 | 39 | To fix this, change the pathPrefix to include a name that starts and ends with "/". 40 | For example: "/document-services/" or "/commerce/cloud-tools/". 41 | 42 | This is required by convention because of the way we construct site URLs. 43 | For example: https://developer.adobe.com + /document-services/ + path/to/files/. 44 | ` 45 | ); 46 | } 47 | } 48 | core.setOutput('path_prefix', pathPrefix); 49 | }; 50 | -------------------------------------------------------------------------------- /.github/super-linter.env: -------------------------------------------------------------------------------- 1 | IGNORE_GITIGNORED_FILES=true 2 | VALIDATE_GITLEAKS=true 3 | VALIDATE_MARKDOWN=true 4 | MARKDOWN_CONFIG_FILE=.markdownlint.yml 5 | VALIDATE_YAML=true 6 | VALIDATE_JSON=true 7 | -------------------------------------------------------------------------------- /.github/workflows/algolia-indexing.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Search Indexing 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | mode: 7 | description: 'Type of indexing. "index" to push to Algolia, "console" for dry run.' 8 | required: true 9 | default: "index" 10 | type: choice 11 | options: 12 | - console 13 | - index 14 | 15 | jobs: 16 | build-and-index: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Node v18 for Yarn v4 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: "18.19.0" # Current LTS version 26 | 27 | - name: Enable Corepack for Yarn 28 | run: corepack enable 29 | 30 | - name: Install Dependencies 31 | run: yarn install 32 | env: 33 | YARN_ENABLE_IMMUTABLE_INSTALLS: false 34 | 35 | - name: Build site 36 | run: yarn build 37 | 38 | env: 39 | NODE_OPTIONS: "--max_old_space_size=8192" 40 | PREFIX_PATHS: true # equivalent to --prefix-paths flag for 'gatsby build' 41 | REPO_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | REPO_OWNER: ${{ github.repository_owner }} 43 | REPO_NAME: ${{ github.event.repository.name }} 44 | REPO_BRANCH: ${{ github.ref_name }} 45 | GATSBY_ALGOLIA_APPLICATION_ID: ${{ secrets.AIO_ALGOLIA_APPLICATION_ID }} 46 | GATSBY_ALGOLIA_SEARCH_API_KEY: ${{ secrets.AIO_ALGOLIA_SEARCH_API_KEY }} 47 | ALGOLIA_WRITE_API_KEY: ${{ secrets.AIO_ALGOLIA_WRITE_API_KEY }} 48 | ALGOLIA_INDEXATION_MODE: ${{ github.event.inputs.mode || 'index' }} 49 | GATSBY_ALGOLIA_INDEX_NAME: ${{ secrets.ALGOLIA_INDEX_NAME || github.event.repository.name }} 50 | GATSBY_FEDS_PRIVACY_ID: ${{ secrets.AIO_FEDS_PRIVACY_ID }} 51 | GATSBY_SITE_DOMAIN_URL: https://developer.adobe.com 52 | -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Github Pages 3 | on: 4 | push: 5 | branches: [main] 6 | jobs: 7 | build-and-deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Yarn Install 13 | uses: bahmutov/npm-install@v1 14 | - name: Build 15 | run: | 16 | yarn build 17 | env: 18 | PREFIX_PATHS: true # works like --prefix-paths flag for 'gatsby build' 19 | PATH_PREFIX: ${{ github.event.repository.name }} 20 | ADOBE_LAUNCH_SRC: ${{ secrets.AIO_ADOBE_LAUNCH_SRC }} 21 | ADOBE_LAUNCH_SRC_INCLUDE_IN_DEVELOPMENT: ${{ secrets.ADOBE_LAUNCH_SRC_INCLUDE_IN_DEVELOPMENT }} 22 | REPO_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | REPO_OWNER: ${{ github.event.repository.owner.login }} 24 | REPO_NAME: ${{ github.event.repository.name }} 25 | REPO_BRANCH: ${{ github.ref_name }} 26 | GOOGLE_OAUTH_CLIENT_ID: ${{ secrets.GOOGLE_OAUTH_CLIENT_ID }} 27 | GOOGLE_OAUTH_CLIENT_SECRET: ${{ secrets.GOOGLE_OAUTH_CLIENT_SECRET }} 28 | GOOGLE_DOCS_TOKEN: ${{ secrets.GOOGLE_DOCS_TOKEN }} 29 | GOOGLE_DOCS_FOLDER_ID: ${{ secrets.GOOGLE_DOCS_FOLDER_ID }} 30 | - name: Deploy to GH Pages 31 | uses: JamesIves/github-pages-deploy-action@v4 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | branch: gh-pages # The branch the action should deploy to. 35 | folder: public # The folder the action should deploy. 36 | clean: true # Automatically remove deleted files from deploy branch 37 | - name: GH Pages URL 38 | id: gh-pages-url 39 | run: | 40 | echo "View GH-Pages: $(https://adobedocs.github.io/${{ github.event.repository.name }})" 41 | -------------------------------------------------------------------------------- /.github/workflows/test-pull-request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ########################### 3 | ########################### 4 | ## Pull request testing ## 5 | ########################### 6 | ########################### 7 | name: Latest Pull Request 8 | 9 | # Documentation: 10 | # - Workflow: https://help.github.com/en/articles/workflow-syntax-for-github-actions 11 | # - SuperLinter: https://github.com/github/super-linter 12 | # - Markdown linter: https://github.com/DavidAnson/markdownlint 13 | # - Link validation: https://github.com/remarkjs/remark-validate-links 14 | 15 | ###################################################### 16 | # Start the job on a pull request to the main branch # 17 | ###################################################### 18 | on: 19 | pull_request: 20 | branches: [main] 21 | 22 | ############### 23 | # Set the Job # 24 | ############### 25 | jobs: 26 | validate: 27 | # Set the agent to run on 28 | runs-on: ubuntu-latest 29 | 30 | ################## 31 | # Load all steps # 32 | ################## 33 | steps: 34 | ########################## 35 | # Checkout the code base # 36 | ########################## 37 | - name: Checkout Code 38 | uses: actions/checkout@v3 39 | with: 40 | # Full git history is needed to get a proper list of changed files 41 | # within `super-linter` 42 | fetch-depth: 0 43 | - run: cat ".github/super-linter.env" >> "$GITHUB_ENV" 44 | 45 | ################################ 46 | # Run Linters against code base # 47 | ################################ 48 | - name: Lint Code Base 49 | # 50 | # Use full version number to avoid cases when a next 51 | # released version is buggy 52 | # About slim image: https://github.com/github/super-linter#slim-image 53 | uses: github/super-linter/slim@v4.10.1 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | DEFAULT_BRANCH: main 57 | VALIDATE_ALL_CODEBASE: false 58 | VALIDATE_GITHUB_ACTIONS: true 59 | 60 | - name: Setup Node v16 for Yarn v3 61 | uses: actions/setup-node@v3 62 | with: 63 | node-version: '16.15.0' # Current LTS version 64 | 65 | - name: Enable Corepack for Yarn v3 66 | run: corepack enable 67 | 68 | - name: Install Yarn v3 69 | uses: borales/actions-yarn@v3 70 | with: 71 | cmd: set version stable 72 | 73 | - name: Install dependencies 74 | uses: borales/actions-yarn@v3 75 | env: 76 | YARN_ENABLE_IMMUTABLE_INSTALLS: false 77 | with: 78 | cmd: install 79 | 80 | - name: Check internal links 81 | uses: borales/actions-yarn@v3 82 | with: 83 | cmd: test:links 84 | 85 | - name: Build site 86 | if: ${{ success() }} 87 | uses: borales/actions-yarn@v3 88 | with: 89 | cmd: build 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # OS and IDE generated files # 3 | ############################## 4 | .DS_Store 5 | .vscode 6 | .history 7 | .idea 8 | .editorconfig 9 | 10 | # npm yarn 11 | node_modules 12 | package.lock 13 | yarn-error.log 14 | .pnp.* 15 | .yarn/* 16 | 17 | # keep in repo 18 | !.gitignore 19 | !.yarn.lock 20 | !.yarnrc.yml 21 | !.yarn/patches 22 | !.yarn/plugins 23 | !.yarn/releases 24 | !.yarn/sdks 25 | !.yarn/versions 26 | 27 | # gatsby files 28 | .env 29 | .cache 30 | public 31 | 32 | # cypress 33 | cypress/videos 34 | cypress/screenshots 35 | 36 | # lerna 37 | lerna-debug.log 38 | 39 | # local actions 40 | .actrc 41 | .secrets 42 | local-test.yml 43 | 44 | # yalc 45 | .yalc 46 | yalc.lock -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | nodeLinker: node-modules 3 | yarnPath: .yarn/releases/yarn-3.2.2.cjs 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Adobe Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language. 18 | * Being respectful of differing viewpoints and experiences. 19 | * Gracefully accepting constructive criticism. 20 | * Focusing on what is best for the community. 21 | * Showing empathy towards other community members. 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances. 27 | * Trolling, insulting/derogatory comments, and personal or political attacks. 28 | * Public or private harassment. 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission. 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting. 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Grp-opensourceoffice@adobe.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version]. 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | © Copyright 2015-2020 Adobe. All rights reserved. 2 | 3 | Adobe holds the copyright for all the files found in this repository. 4 | 5 | See the LICENSE file for licensing information. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Lightroom Services 2 | 3 | Adobe Photoshop Lightroom stores user _assets_, with their associated metadata and media renditions, in a _catalog_ in the cloud. 4 | 5 | ### Accessing Lightroom Content 6 | 7 | Lightroom content of a Creative Cloud customer is managed through a set of RESTful APIs. These APIs are available only to entitled partner applications that have authenticated the customer, and the customer has given their express permission to the client to act on their behalf. The [API Reference](https://developer.adobe.com/lightroom/lightroom-api-docs/api/) documents the available APIs. 8 | 9 | Partners must register a new _integration_ with Adobe to obtain a unique client identifier (_API key_) for their application. Partner applications authenticate Lightroom customers through the Adobe Identity Management System (IMS) using a standard OAuth 2.0 workflow. This process enables a client to obtain an _access token_ that must be included along with the integration API Key in privileged requests to the Lightroom APIs. 10 | 11 | 12 | ## Where to ask for help 13 | 14 | The slack channel #adobeio-onsite-onboarding is our main point of contact for help. Feel free to join the channel and ask any questions. 15 | 16 | ## How to develop 17 | 18 | For local development, simply use : 19 | 20 | ```shell 21 | $ yarn install 22 | $ yarn dev 23 | ``` 24 | 25 | For the developer documentation, read the following sections on how to: 26 | 27 | - [Arrange the structure content of your docs](https://github.com/adobe/aio-theme#content-structure) 28 | - [Link to pages](https://github.com/adobe/aio-theme#links) 29 | - [Use assets](https://github.com/adobe/aio-theme#assets) 30 | - [Set global Navigation](https://github.com/adobe/aio-theme#global-navigation) 31 | - [Set side navigation](https://github.com/adobe/aio-theme#side-navigation) 32 | - [Use content blocks](https://github.com/adobe/aio-theme#jsx-blocks) 33 | - [Use Markdown](https://github.com/adobe/aio-theme#writing-enhanced-markdown) 34 | 35 | For more in-depth [instructions](https://github.com/adobe/aio-theme#getting-started). 36 | 37 | ## How to test 38 | 39 | - To run the configured linters locally (requires [Docker](https://www.docker.com/)): 40 | 41 | ```shell 42 | yarn lint 43 | ``` 44 | 45 | > NOTE If you cannot use Docker, you can install the linters separately. In `.github/super-linter.env`, see which linters are enabled, and find the tools being used for linting in [Supported Linters](https://github.com/github/super-linter#supported-linters). 46 | 47 | - To check internal links locally 48 | 49 | ```shell 50 | yarn test:links 51 | ``` 52 | 53 | - To build and preview locally: 54 | 55 | ```shell 56 | yarn start 57 | ``` 58 | 59 | ## How to deploy 60 | 61 | For any team that wishes to deploy to the developer.adobe.com and developer-stage.adobe.com websites, they must be in contact with the dev-site team. Teams will be given a path that will follow the pattern `developer.adobe.com/{product}/`. This will allow doc developers to setup their subpaths to look something like: 62 | 63 | ```text 64 | developer.adobe.com/{product}/docs 65 | developer.adobe.com/{product}/community 66 | developer.adobe.com/{product}/community/code_of_conduct 67 | developer.adobe.com/{product}/community/contribute 68 | ``` 69 | 70 | ### Launching a deploy 71 | 72 | You can deploy using the GitHub actions deploy workflow see [deploy instructions](https://github.com/adobe/aio-theme#deploy-to-azure-storage-static-websites). 73 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | module.exports = { 14 | siteMetadata: { 15 | pages: [ 16 | { 17 | title: 'Lightroom API Overview', 18 | path: '/' 19 | }, 20 | { 21 | title: 'Get Started', 22 | path: '/getting-started/' 23 | }, 24 | { 25 | title: 'Guides', 26 | path: '/guides/' 27 | }, 28 | { 29 | title: 'Code Samples', 30 | path: '/code-sample/' 31 | }, 32 | { 33 | title: 'API Reference', 34 | path: '/api/index.md' 35 | }, 36 | { 37 | title: 'Release Notes', 38 | path: '/release-notes/' 39 | } 40 | ], 41 | subPages: [ 42 | { 43 | title: 'Getting Started', 44 | path: '/getting-started/', 45 | pages: [ 46 | { 47 | title: 'Create an Integration', 48 | path: '/getting-started/create_integration/' 49 | }, 50 | { 51 | title: 'Services Health Check', 52 | path: '/getting-started/service_health_check/' 53 | }, 54 | { 55 | title: 'Authenticate Customers', 56 | path: '/getting-started/authenticate_customers/' 57 | }, 58 | { 59 | title: 'Upload Content', 60 | path: '/getting-started/upload_content/' 61 | }, 62 | { 63 | title: 'Manage Content', 64 | path: '/getting-started/manage_content/' 65 | }, 66 | { 67 | title: 'Read and Generate Renditions', 68 | path: '/getting-started/read_generate_renditions/' 69 | }, 70 | { 71 | title: 'Developer Guidelines', 72 | path: '/getting-started/developer_guidelines/' 73 | }, 74 | { 75 | title: 'Branding Guidelines', 76 | path: '/getting-started/branding_guidelines/' 77 | } 78 | ] 79 | }, 80 | { 81 | title: 'Using the Lightroom APIs', 82 | path: '/guides/', 83 | pages: [ 84 | { 85 | title: 'Calling a Lightroom API', 86 | path: '/guides/calling_api/' 87 | }, 88 | { 89 | title: 'Primitive Data Types', 90 | path: '/guides/primitive_data_types/' 91 | }, 92 | { 93 | title: 'Common Data Model', 94 | path: '/guides/common_data_model/' 95 | }, 96 | { 97 | title: 'Links and Pagination', 98 | path: '/guides/links_and_pagination/' 99 | } 100 | ] 101 | }, 102 | { 103 | title: 'Sample Code', 104 | path: '/code-sample/' 105 | }, 106 | { 107 | title: 'API Change Logs', 108 | path: '/release-notes/' 109 | } 110 | ] 111 | }, 112 | plugins: [`@adobe/gatsby-theme-aio`], 113 | pathPrefix: process.env.PATH_PREFIX || '/lightroom/lightroom-api-docs/' 114 | }; 115 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import React from 'react'; 14 | import {withPrefix} from 'gatsby'; 15 | 16 | export const onRenderBody = ({setHeadComponents}) => { 17 | setHeadComponents([ 18 | 19 | ]); 20 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "lightroom-public-api", 4 | "version": "1.0.0", 5 | "license": "Apache-2.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/AdobeDocs/lightroom-public-apis" 9 | }, 10 | "author": { 11 | "name": "Samyak Bagra", 12 | "url": "https://github.com/bagra98" 13 | }, 14 | "dependencies": { 15 | "@adobe/gatsby-theme-aio": "^4.14.4", 16 | "gatsby": "4.22.0", 17 | "react": "^17.0.2", 18 | "react-dom": "^17.0.2" 19 | }, 20 | "resolutions": { 21 | "sharp": "0.33.0", 22 | "gatsby-sharp": "1.12.0" 23 | }, 24 | "scripts": { 25 | "start": "gatsby build && gatsby serve", 26 | "start:prefix": "gatsby build --prefix-paths && gatsby serve --prefix-paths", 27 | "dev": "gatsby develop", 28 | "dev:https": "gatsby develop --https --host localhost.corp.adobe.com --port 9000", 29 | "build": "gatsby build", 30 | "serve": "gatsby serve", 31 | "clean": "gatsby clean", 32 | "test:links": "remark src/pages --quiet --frail", 33 | "lint": "docker run --rm -e RUN_LOCAL=true --env-file '.github/super-linter.env' -v \"$PWD\":/tmp/lint github/super-linter:slim-v4.10.1" 34 | }, 35 | "remarkConfig": { 36 | "plugins": [ 37 | "remark-validate-links" 38 | ] 39 | }, 40 | "packageManager": "yarn@3.2.2", 41 | "devDependencies": { 42 | "remark-cli": "^11.0.0", 43 | "remark-validate-links": "^12.1.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /samples/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | © Copyright 2020 Adobe. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /samples/README.md: -------------------------------------------------------------------------------- 1 | # Lightroom Partner API Samples 2 | 3 | This directory contains various examples using the Lightroom Partner APIs. The samples are all written in JavaScript, and divided into two major categories: Node.js command-line samples in `src/node` and web application samples in `src/web`. There is a corresponding README in each of those directories. Common code shared among the samples is found in `src/common` and described below. The bulk of the Lightroom operations are handled by the common code, and most of the Node.js samples are thin wrappers around them. 4 | 5 | ## Prerequisites 6 | 7 | * Configuring a system for development, such as installing Node.js and managing browser compatibility is outside the scope of these samples. It is assumed this has been completed. 8 | 9 | * Developers must create an application integration with the `Lightroom Services` as described in the [Adobe Lightroom developer documentation](https://developer.adobe.com/lightroom). This process will generate an `API Key` and `Client Secret`. 10 | 11 | * Developers must acquire an OAuth 2.0 `user access token` for a Lightroom customer as detailed in the [Adobe Developer authentication documentation](https://developer.adobe.com/authentication/auth-methods.html#!AdobeDocs/adobeio-auth/master/OAuth/OAuth.md), with the `openid`, `lr_partner_apis` and `lr_partner_rendition_apis` scopes. 12 | 13 | ## Lightroom Common Code 14 | 15 | Common code for making requests and managing Lightroom content can be found `src/common/lr`. They are: 16 | 17 | * `LrAuth.mjs`: This module provides a simple abstraction to the authentication process that a full-featured application might use. In this case, it simply fetches the API Key and user access token from the `process.env` and promises to return them in a `session`. 18 | 19 | * `LrRequestor.mjs`: This module makes requests to the `lr.adobe.io` endpoints, parses JSON return data when needed, and throws an error when a request has failed. It takes in the `session` returned by `LrAuth.js` to construct the proper requests. 20 | 21 | * `LrSession.mjs`: This module authenticates a user via `LrAuth.mjs` and validates that they are entitled to use Lightroom (their account has either a `trial` or `subscriber` status) and that they have a catalog. It will throw an error if these conditions are not satisfied, and return an `LrContext.mjs` object on success. 22 | 23 | * `LrContext.mjs`: Set of application-friendly wrappers for calling the methods in `LrRequestor.mjs`. Constructs API paths, forms request bodies when needed, and traps expected error conditions. 24 | 25 | * `LrUtils.mjs`: A collection of different utlities that applications might find useful. 26 | 27 | ## HTTP Request Common Code 28 | 29 | Common code for handling HTTP requests is found in `src/common/request`. It consists of: 30 | 31 | * `RequestUtils.mjs`: Utilities for making promise-wrapped HTTP requests. Contains both Node.js and web browser implementation, which are dynamically loaded depending on the current environment. 32 | 33 | ## Cryptographic Common Code 34 | 35 | Shared cryptographic code is found in `src/common/cyrpto`. It consists of: 36 | 37 | * `CryptoUtils.mjs`: Utilities for generating a universally unique identifier that conforms to the Lightroom Services APIs and for computing the SHA-256 hash of a buffer of streamed buffer of data. Contains both Node.js and web browser implementation, which are dynamically loaded depending on the current environment. 38 | 39 | ## Original Common Code 40 | 41 | Shared code for handling original photos and videos that will be uploaded to Lightroom is found in `src/common/original`. It consists of: 42 | 43 | * `OriginalUtils.mjs`: Utilities for creating an object that describes original media (such as subtype or upload MIME type) and to upload these originals with the help of methods found in `LrContext.mjs`. 44 | -------------------------------------------------------------------------------- /samples/doc.md: -------------------------------------------------------------------------------- 1 | ## Lightroom Services Sample Code 2 | 3 | Sample code demonstrating the use of the Lightroom Services APIs can be found in a [subdirectory](https://github.com/AdobeDocs/lightroom-partner-apis/tree/master/samples) of the Lightroom Services [GitHub repository](https://github.com/AdobeDocs/lightroom-partner-apis/tree/master). 4 | -------------------------------------------------------------------------------- /samples/partner_days_2021/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | server/cert.pem 3 | server/key.pem 4 | -------------------------------------------------------------------------------- /samples/partner_days_2021/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adobe-auth-node", 3 | "version": "1.0.0", 4 | "description": "adobe auth node example", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./server/index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "body-parser": "^1.18.2", 14 | "express": "^4.16.3", 15 | "express-session": "^1.15.6", 16 | "jade": "^1.11.0", 17 | "request": "^2.85.0", 18 | "request-promise": "^4.2.2", 19 | "uuid": "^8.3.2", 20 | "multer": "^1.4.2", 21 | "stream-buffers": "^3.0.2", 22 | "stream": "^0.0.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/partner_days_2021/readme.md: -------------------------------------------------------------------------------- 1 | # Lightroom Partner App Example: Node.js 2 | 3 | This sample app will show you how to implement Lightroom Partner App and Adobe OAuth 2.0 using Node.js. 4 | 5 | After setting up the sample, you will have a Node.js app that: 6 | 7 | 1. Runs on `https://localhost:8000` 8 | 1. Lets a user log in with their Adobe ID 9 | 1. Prompts the user to authorize the app with requested scopes 10 | 1. Lets the user view their account information 11 | 1. Lets the user view their catalog information 12 | 1. Lets the user upload an image to their catalog 13 | 1. Lets the user download different renditions of their uploaded image 14 | 15 | 16 | 17 | 18 | 19 | 20 | ## Contents 21 | 22 | 1. [Technology Used](#technologyused) 23 | 1. [Prerequisites](#prerequisites) 24 | 1. [Configuration](#configuration) 25 | 1. [Create an OpenSSL cert](#createanopensslcert) 26 | 1. [Install Node.js packages](#installnodejspackages) 27 | 1. [Enter your Adobe API credentials](#enteryouradobeapicredentials) 28 | 1. [Usage](#usage) 29 | 30 | 31 | 32 | ## Technology Used 33 | 34 | 1. Node.js and the `npm` package manager 35 | 1. OpenSSL CLI 36 | 37 | ## Prerequisites 38 | 39 | This guide will assume that you have read the [Integration Document](https://developer.adobe.com/lightroom/lightroom-api-docs/getting-started/create_integration/). 40 | 41 | You must also have a registered app on the Adobe Developer Console with the following settings: 42 | 43 | 1. `Platform`: web 44 | 1. `Default redirect URI`: `https://localhost:8000` 45 | 1. `Redirect URI Pattern`: `https://localhost:8000` 46 | 47 | ## Configuration 48 | 49 | The following steps will help you get this sample up and running. 50 | 51 | ### Create an OpenSSL cert 52 | 53 | Adobe OAuth 2.0 requires SSL, so you will need to create a self-signed cert using the OpenSSL CLI. Be sure to run this in the `./server` directory: 54 | 55 | ``` 56 | $ cd server 57 | $ openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365 58 | ``` 59 | 60 | Make sure that after running this command you have the `cert.pem` and `key.pem` files at the top level of the `.server` directory. 61 | 62 | ### Install Node.js packages 63 | 64 | The `package.json` file contains a list of dependencies. Run the following command from the top level directory of the app to install these dependencies: 65 | 66 | ``` 67 | $ cd .. 68 | $ npm install 69 | ``` 70 | 71 | ### Enter your Adobe API credentials 72 | 73 | Enter the required credentials in `public/config.js`: 74 | 75 | ```javascript 76 | const adobeApiKey = "YOUR_API_KEY"; 77 | const adobeApiSecret = "YOUR_API_SECRET"; 78 | 79 | try { 80 | if (module) { 81 | module.exports = { 82 | adobeApiKey: adobeApiKey, 83 | adobeApiSecret: adobeApiSecret, 84 | } 85 | } 86 | } 87 | catch (err) {} 88 | ``` 89 | 90 | You can get your Adobe API Key and Secret from your registered app page on the [Adobe Developer Console](https://console.adobe.io). 91 | 92 | 93 | ## Usage 94 | 95 | After completing the configuration steps, start the server: 96 | 97 | ``` 98 | $ npm start 99 | ``` 100 | 101 | To access the app, go to `https://localhost:8000`. Click through any cert warnings in the browser. 102 | -------------------------------------------------------------------------------- /samples/partner_days_2021/server/index.js: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid') 2 | 3 | const express = require('express'); 4 | const app = express(); 5 | const session = require('express-session') 6 | const request = require('request-promise'); 7 | const https = require('https'); 8 | const bodyParser = require('body-parser'); 9 | const adobeApiKey = require('../public/config.js').adobeApiKey; 10 | const adobeApiSecret = require('../public/config.js').adobeApiSecret; 11 | const fs = require('fs'); 12 | const path = require('path'); 13 | const multer = require('multer') 14 | 15 | /* Declare host name and port */ 16 | const hostname = 'localhost'; 17 | const port = 8000; 18 | 19 | 20 | const outputFolderName = './public/uploads'; 21 | const upload = multer({ 22 | storage: multer.diskStorage({ 23 | destination: outputFolderName, 24 | filename: (req, file, cb) => cb(null, file.originalname), 25 | }), 26 | }); 27 | /* Middlewares */ 28 | app.use(bodyParser.json()); 29 | app.use(express.static(path.join(__dirname, '../public'))); 30 | app.use(express.urlencoded({ extended: true })) 31 | app.use(upload.single('file')); 32 | app.set('views', path.join(__dirname, '../views')) 33 | app.set('view engine', 'jade') 34 | app.use(session({ 35 | /* Change this to your own secret value */ 36 | secret: 'this-is-secret', 37 | resave: true, 38 | saveUninitialized: true, 39 | cookie: { 40 | secure: false, 41 | maxAge: 6000000 42 | } 43 | })); 44 | 45 | /* Routes */ 46 | app.get('/', function (req, res) { 47 | fs.unlink('./public/images/rendition.jpeg', function(err){ 48 | console.log(err) 49 | }) 50 | res.render('index'); 51 | }) 52 | 53 | app.get('/login', function(req, res){ 54 | /* This will prompt user with the Adobe auth screen */ 55 | res.redirect(`https://ims-na1.adobelogin.com/ims/authorize?client_id=${adobeApiKey}&scope=openid,AdobeID,lr_partner_apis,lr_partner_rendition_apis&response_type=code&redirect_uri=https://localhost:8000/callback`) 56 | }) 57 | 58 | app.get('/callback', function(req, res){ 59 | /* Retrieve authorization code from request */ 60 | let code = req.query.code; 61 | 62 | /* Set options with required paramters */ 63 | let requestOptions = { 64 | uri: `https://ims-na1.adobelogin.com/ims/token/v3?grant_type=authorization_code&client_id=${adobeApiKey}&client_secret=${adobeApiSecret}&code=${code}`, 65 | method: 'POST', 66 | json: true 67 | } 68 | 69 | /* Send a POST request using the request library */ 70 | request(requestOptions) 71 | .then(function (response) { 72 | /* Store the token in req.session.token */ 73 | req.session.token = response.access_token; 74 | console.log(response.access_token) 75 | res.render('index', {'response':'User logged in!'}); 76 | }) 77 | .catch(function (error) { 78 | console.log(error) 79 | res.render('index', {'response':'Log in failed!'}); 80 | }); 81 | }) 82 | 83 | app.get('/account', function(req, res){ 84 | if (req.session.token) { 85 | /* Grab the token stored in req.session 86 | and set options with required parameters */ 87 | 88 | let requestOptions = { 89 | uri: `https://lr.adobe.io/v2/account`, 90 | headers: { 91 | Authorization: `Bearer ${req.session.token}`, 92 | 'X-Api-Key': adobeApiKey 93 | }, 94 | json: true 95 | }; 96 | console.log(requestOptions) 97 | 98 | /* Send a GET request using the request library */ 99 | request(requestOptions) 100 | .then(function (response) { 101 | console.log(response) 102 | /* Send the received response back to the client side */ 103 | response = response.replace("while (1) {}", "") 104 | response = response.replace("\n", "") 105 | let parsed_response = JSON.parse(response) 106 | let account_id = parsed_response.id 107 | req.session.account_id = account_id; 108 | res.render('index', {'response':"Account details fetched.", 'response_second': " \nAccount email is " + JSON.stringify(parsed_response.email) + " \nand Account ID is " +JSON.stringify(account_id)}); 109 | }) 110 | .catch(function (error) { 111 | console.log(error) 112 | res.render('index', {'response':'Failed to get account'}); 113 | }); 114 | 115 | } else { 116 | res.render('index', {'response':'You need to log in first'}); 117 | } 118 | }) 119 | 120 | app.get('/catalog', function(req, res){ 121 | if (req.session.token) { 122 | /* Grab the token stored in req.session 123 | and set options with required parameters */ 124 | let requestOptions = { 125 | uri: `https://lr.adobe.io/v2/catalog`, 126 | headers: { 127 | Authorization: `Bearer ${req.session.token}`, 128 | 'X-Api-Key': adobeApiKey 129 | }, 130 | json: true 131 | }; 132 | console.log(requestOptions) 133 | 134 | /* Send a GET request using the request library */ 135 | request(requestOptions) 136 | .then(function (response) { 137 | /* Send the received response back to the client side */ 138 | console.log(response) 139 | response = response.replace("while (1) {}", "") 140 | response = response.replace("\n", "") 141 | let parsed_response = JSON.parse(response) 142 | let catalog_id = parsed_response.id 143 | req.session.catalog_id = catalog_id; 144 | res.render('index', {'response': "Catalog Details Fetched", 'response_second': "Catalog ID is " + JSON.stringify(catalog_id)}); 145 | }) 146 | .catch(function (error) { 147 | console.log(error) 148 | res.render('index', {'response':'Failed to get catalog'}); 149 | }); 150 | 151 | } else { 152 | res.render('index', {'response':'You need to log in first'}); 153 | } 154 | }) 155 | 156 | 157 | app.get('/albums', function(req, res){ 158 | if (req.session.token) { 159 | /* Grab the token stored in req.session 160 | and set options with required parameters */ 161 | let requestOptions = { 162 | uri: `https://lr.adobe.io/v2/catalogs/${req.session.catalog_id}/albums`, 163 | headers: { 164 | Authorization: `Bearer ${req.session.token}`, 165 | 'X-Api-Key': adobeApiKey 166 | }, 167 | json: true 168 | }; 169 | console.log(requestOptions) 170 | /* Send a GET request using the request library */ 171 | request(requestOptions) 172 | .then(function (response) { 173 | /* Send the received response back to the client side */ 174 | console.log(response) 175 | response = response.replace("while (1) {}", "") 176 | response = response.replace("\n", "") 177 | let parsed_response = JSON.parse(response) 178 | let albums_names = [] 179 | parsed_response.resources.forEach((x, i) => albums_names.push(x.payload.name)); 180 | console.log(albums_names) 181 | res.render('index', {'response': "Albums Fetched", 'response_second':JSON.stringify(albums_names.join(', '))}); 182 | }) 183 | .catch(function (error) { 184 | console.log(error) 185 | res.render('index', {'response':'Failed to get albums'}); 186 | }); 187 | 188 | } else { 189 | res.render('index', {'response':'You need to log in first'}); 190 | } 191 | }) 192 | 193 | app.post('/create_asset', function(req, res){ 194 | if (!req.session.token) { 195 | res.render('index', {'response':'You need to log in first'}); 196 | return 197 | } 198 | if (!req.session.catalog_id || !req.session.account_id) { 199 | res.render('index', {'response':'You need get account as well as catalog first'}); 200 | return 201 | } 202 | 203 | let asset_uuid = uuid.v4(); 204 | asset_uuid = asset_uuid.replace(/-/g, "") 205 | console.log('Your Asset UUID is: ' + asset_uuid); 206 | if (!req.file){ 207 | res.render('index', {'response':'Please select a file first'}); 208 | return 209 | } 210 | let content = { 211 | subtype: "image", 212 | payload: { 213 | captureDate: '0000-00-00T00:00:00', 214 | userCreated: "2019-09-17T17:46:38.575399Z", 215 | userUpdated: "2019-09-17T17:46:38.575399Z", 216 | importSource: { 217 | fileName:req.file.originalname, 218 | importTimestamp: "2019-09-17T17:46:38.575399Z", 219 | importedBy: req.session.account_id, 220 | importedOnDevice: adobeApiKey 221 | } 222 | } 223 | } 224 | let requestOptions = { 225 | uri: `https://lr.adobe.io/v2/catalogs/${req.session.catalog_id}/assets/${asset_uuid}`, 226 | headers: { 227 | Authorization: `Bearer ${req.session.token}`, 228 | 'X-Api-Key': adobeApiKey 229 | }, 230 | body: content, 231 | json: true, 232 | method: "PUT" 233 | }; 234 | 235 | console.log(requestOptions) 236 | 237 | request(requestOptions) 238 | .then(function (response) { 239 | /* Send the received response back to the client side */ 240 | console.log(response) 241 | let requestOptionsMaster = { 242 | uri: `https://lr.adobe.io/v2/catalogs/${req.session.catalog_id}/assets/${asset_uuid}/master`, 243 | headers: { 244 | Authorization: `Bearer ${req.session.token}`, 245 | 'X-Api-Key': adobeApiKey, 246 | 'Content-Type': 'application/octet-stream' 247 | }, 248 | body: fs.createReadStream("./public/uploads/"+ req.file.originalname), 249 | //body: stream, 250 | method: "PUT" 251 | }; 252 | 253 | console.log(requestOptionsMaster) 254 | 255 | request(requestOptionsMaster) 256 | .then(function (response) { 257 | /* Send the received response back to the client side */ 258 | console.log(response) 259 | req.session.asset_id = asset_uuid; 260 | res.render('index', {'response': "Image asset created for file name " +req.file.originalname}); 261 | }) 262 | .catch(function (error) { 263 | console.log(error) 264 | res.render('index', {'response':'Failed to craete asset'}); 265 | }); 266 | }) 267 | .catch(function (error) { 268 | console.log(error) 269 | res.render('index', {'response':'Failed to craete asset'}); 270 | }); 271 | }) 272 | 273 | app.post('/fetch_rendition', function(req, res){ 274 | if (!req.session.token) { 275 | res.render('index', {'response':'You need to log in first'}); 276 | return 277 | } 278 | 279 | if (!req.session.catalog_id || !req.session.asset_id) { 280 | res.render('index', {'response':'You need fetch catalog first and upload an image'}); 281 | return 282 | } 283 | 284 | let requestOptions = { 285 | uri: `https://lr.adobe.io/v2/catalogs/${req.session.catalog_id}/assets/${req.session.asset_id}/renditions/${req.body.rendition_type}`, 286 | headers: { 287 | Authorization: `Bearer ${req.session.token}`, 288 | 'X-Api-Key': adobeApiKey 289 | } 290 | }; 291 | 292 | console.log(requestOptions) 293 | request(requestOptions).on('response', function(response){ 294 | //Print response if needed 295 | }).pipe(fs.createWriteStream("./public/images/rendition.jpeg")).on('close', function(err) { 296 | if (err){ 297 | console.log(err) 298 | res.render('index', {'response':'Failed to get rendition'}); 299 | } else { 300 | res.render('index', {'response': "Rendition Fetched", 'rendition': true}); 301 | } 302 | }); 303 | }) 304 | 305 | /* Set up a HTTS server with the signed certification */ 306 | var httpsServer = https.createServer({ 307 | key: fs.readFileSync(path.join(__dirname, 'key.pem')), 308 | cert: fs.readFileSync(path.join(__dirname, 'cert.pem')) 309 | }, app).listen(port, hostname, (err) => { 310 | if (err) console.log(`Error: ${err}`); 311 | console.log(`listening on port ${port}!`); 312 | }); -------------------------------------------------------------------------------- /samples/partner_days_2021/views/index.jade: -------------------------------------------------------------------------------- 1 | doctype 2 | html 3 | 4 | head 5 | title Partner App Example 6 | link(rel='stylesheet', href='/stylesheets/style.css') 7 | body 8 | header 9 | h1 Partner App Example 10 | .button-group 11 | button(onclick="location.href='/login'") Log In with Adobe 12 | button(onclick="location.href='/account'") Get Account 13 | button(onclick="location.href='/catalog'") Get Catalog 14 | button(onclick="location.href='/albums'") Get All Albums 15 | br 16 | br 17 | form(action='/create_asset',method='post', enctype="multipart/form-data") 18 | input(id='fileUpload',type='file',name='file') 19 | button(type="submit") Upload an Image 20 | br 21 | br 22 | form(action='/fetch_rendition',method='post') 23 | //span.label Enter Rendition Type (640,1280,2048): 24 | input(id='rendition_type',type='input',name='rendition_type', value='Enter Rendition Type') 25 | button(type="submit") Fetch Image Asset Rendition 26 | .canvas 27 | p #{response} 28 | p #{response_second} 29 | if rendition 30 | img.image(src='/' + "images/rendition.jpeg" ) 31 | -------------------------------------------------------------------------------- /samples/src/common/crypto/CryptoUtils.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | let CryptoUtils = {} 12 | 13 | let node 14 | try { node = process.versions.node } catch (err) {} 15 | 16 | if (node) { 17 | // import crypto from 'crypto' // could use static import if we are only on node 18 | let crypto, _cryptoPromise = import('crypto').then(module => crypto = module) 19 | 20 | let _uuid = () => { 21 | // helper function for generating a Lightroom unique identifier 22 | let bytes = crypto.randomBytes(16) 23 | bytes[6] = bytes[6] & 0x0f | 0x40 24 | bytes[8] = bytes[8] & 0x3f | 0x80 25 | return bytes.toString('hex') 26 | } 27 | 28 | let _sha256P = async (streamP) => { 29 | const sha256 = crypto.createHash('sha256') 30 | let chunkSize = 20 * 1024 * 1024 // 20 MB 31 | let chunkHandlerP = data => sha256.update(data) 32 | await streamP(chunkSize, chunkHandlerP) 33 | return sha256.digest('hex') 34 | } 35 | 36 | CryptoUtils.uuidP = () => _cryptoPromise.then(() => _uuid()) 37 | CryptoUtils.sha256P = (streamP) => _cryptoPromise.then(() => _sha256P(streamP)) 38 | } 39 | else { 40 | let _toStringHex = (bytes) => { 41 | let str = '' 42 | for (let byte of bytes) { 43 | const hex = byte.toString(16) 44 | str += (hex.length == 1) ? '0' + hex : hex 45 | } 46 | return str 47 | } 48 | 49 | CryptoUtils.uuidP = async () => { // async to mimic node version above 50 | let bytes = new Uint8Array(16) 51 | crypto.getRandomValues(bytes) 52 | bytes[6] = bytes[6] & 0x0f | 0x40 53 | bytes[8] = bytes[8] & 0x3f | 0x80 54 | return _toStringHex(bytes) 55 | } 56 | 57 | CryptoUtils.sha256P = async (streamP) => { 58 | let subtle = window.crypto.subtle 59 | if (!subtle) { 60 | return Promise.reject(new Error('missing crypto.subtle')) 61 | } 62 | let chunkSize = 0 // can only handle one chunk with subtle 63 | let digest 64 | let chunkHandlerP = async (data, offset = 0) => { 65 | if (digest) { 66 | return Promise.reject(new Error('can only hash one chunk')) 67 | } 68 | digest = await subtle.digest('SHA-256', data) 69 | } 70 | await streamP(chunkSize, chunkHandlerP) 71 | return _toStringHex(new Uint8Array(digest)) 72 | } 73 | } 74 | 75 | export default CryptoUtils 76 | -------------------------------------------------------------------------------- /samples/src/common/lr/LrAuth.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | let preauthenticatedHost = 'lr.adobe.io' 12 | let preauthenticatedApiKey 13 | let preauthenticatedToken 14 | 15 | if (process && process.env) { 16 | preauthenticatedHost = process.env.HOST || preauthenticatedHost 17 | preauthenticatedApiKey = process.env.KEY || preauthenticatedApiKey 18 | preauthenticatedToken = process.env.TOKEN || preauthenticatedToken 19 | } 20 | 21 | let LrAuth = { 22 | getHost: () => preauthenticatedHost, 23 | getApiKey: () => preauthenticatedApiKey, 24 | 25 | authenticateP: async () => { 26 | return { 27 | host: preauthenticatedHost, 28 | apiKey: preauthenticatedApiKey, 29 | accessToken: preauthenticatedToken 30 | } 31 | } 32 | } 33 | 34 | export default LrAuth 35 | -------------------------------------------------------------------------------- /samples/src/common/lr/LrContext.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrRequestor from './LrRequestor.mjs' 12 | import CryptoUtils from '../crypto/CryptoUtils.mjs' 13 | 14 | class LrContext { 15 | constructor(session, account, catalog) { 16 | this.account = account 17 | this.catalog = catalog 18 | this.chunkSize = 20 * 1024 * 1024 // minimum chunk size of 32K except last one 19 | 20 | this._session = session 21 | this._accountId = account.id 22 | this._catalogId = catalog.id 23 | } 24 | 25 | getFirstPageOfAssetsP() { 26 | let path = `/v2/catalogs/${this._catalogId}/assets?subtype=image%3Bvideo` 27 | return LrRequestor.getP(this._session, path).then((response) => response.resources) 28 | } 29 | 30 | getFirstAssetP() { 31 | let path = `/v2/catalogs/${this._catalogId}/assets?subtype=image%3Bvideo&limit=1` 32 | return LrRequestor.getP(this._session, path) 33 | } 34 | 35 | getAssetP(assetId) { 36 | let path = `/v2/catalogs/${this._catalogId}/assets/${assetId}` 37 | return LrRequestor.getP(this._session, path) 38 | } 39 | 40 | getIncompletesP() { 41 | let path = `/v2/catalogs/${this._catalogId}/assets?subtype=image%3Bvideo&exclude=complete` 42 | return LrRequestor.getPagedP(this._session, path) 43 | } 44 | 45 | async createRevisionP(subtype, name, size, sha256) { 46 | let assetId = await CryptoUtils.uuidP() 47 | let path = `/v2/catalogs/${this._catalogId}/assets/${assetId}` 48 | let importTimestamp = (new Date()).toISOString() 49 | let content = { 50 | subtype: subtype, 51 | payload: { 52 | captureDate: '0000-00-00T00:00:00', 53 | userCreated: importTimestamp, 54 | userUpdated: importTimestamp, 55 | importSource: { 56 | fileName: name, 57 | importTimestamp: importTimestamp, 58 | importedBy: this._accountId, 59 | importedOnDevice: this._session.apiKey 60 | } 61 | } 62 | } 63 | if (subtype == 'video') { 64 | content.payload.importSource.contentType = 'video/*' 65 | if (size) { 66 | content.payload.importSource.fileSize = size 67 | } 68 | if (sha256) { 69 | content.payload.importSource.sha256 = sha256 70 | } 71 | } 72 | await LrRequestor.putUniqueP(this._session, path, content, sha256) // 412 error if duplicate revision 73 | 74 | return assetId 75 | } 76 | 77 | getAlbumsP(subtype) { 78 | let path = `/v2/catalogs/${this._catalogId}/albums?subtype=${subtype}` 79 | return LrRequestor.getPagedP(this._session, path).then((response) => { 80 | let albums = response.resources 81 | albums.forEach(resource => resource.base = response.base) 82 | return response.resources 83 | }) 84 | } 85 | 86 | getAlbumP(albumId) { 87 | let path = `/v2/catalogs/${this._catalogId}/albums/${albumId}` 88 | return LrRequestor.getP(this._session, path) 89 | } 90 | 91 | async getFirstAlbumAssetP(albumId) { 92 | let path = `/v2/catalogs/${this._catalogId}/albums/${albumId}/assets?subtype=image%3Bvideo&embed=asset&order_after=-&limit=1` 93 | let response = await LrRequestor.getP(this._session, path) 94 | return response.resources[0] 95 | } 96 | 97 | getAlbumAssetsP(albumId) { 98 | let path = `/v2/catalogs/${this._catalogId}/albums/${albumId}/assets?subtype=image%3Bvideo&embed=asset&order_after=-` 99 | return LrRequestor.getPagedP(this._session, path).then(response => response.resources) 100 | } 101 | 102 | async getAlbumCoverP(album) { 103 | if (album.links['/rels/cover_asset']) { 104 | let assetId = album.links['/rels/cover_asset'].href.match(/assets\/([a-f0-9]{32})\/?/)[1] 105 | return this.getAssetRenditionP(assetId, 'thumbnail2x') 106 | } 107 | } 108 | 109 | async getAlbumCoverOrFallbackAssetIdP(album) { 110 | if (album.links['/rels/cover_asset']) { 111 | return album.links['/rels/cover_asset'].href.match(/assets\/([a-f0-9]{32})\/?/)[1] 112 | } 113 | let albumAsset = await this.getFirstAlbumAssetP(album.id) // first asset 114 | return albumAsset?.asset.id // undefined if the album is empty 115 | } 116 | 117 | async createAlbumP (subtype, name, parentId, remoteId) { 118 | let albumId = await CryptoUtils.uuidP() 119 | let path = `/v2/catalogs/${this._catalogId}/albums/${albumId}` 120 | let importTimestamp = (new Date()).toISOString() 121 | let content = { 122 | subtype: subtype, 123 | serviceId: this._session.apiKey, 124 | payload: { 125 | userCreated: importTimestamp, 126 | userUpdated: importTimestamp, 127 | name: name 128 | } 129 | } 130 | if (parentId) { 131 | content.payload.parent = { 132 | id: parentId 133 | } 134 | } 135 | if (remoteId) { 136 | content.payload.publishInfo = { 137 | created: importTimestamp, 138 | updated: importTimestamp, 139 | remoteId: remoteId 140 | } 141 | } 142 | await LrRequestor.putP(this._session, path, content) 143 | return albumId 144 | } 145 | 146 | deleteAlbumP = function(albumId) { 147 | let path = `/v2/catalogs/${this._catalogId}/albums/${albumId}` 148 | return LrRequestor.deleteP(this._session, path) 149 | } 150 | 151 | putOriginalP(assetId, mime, size, streamP) { 152 | let path = `/v2/catalogs/${this._catalogId}/assets/${assetId}/master` 153 | let chunkHandlerP = (data, offset) => { 154 | console.log(`Received ${data.byteLength} bytes of data at ${offset}`) 155 | return LrRequestor.putChunkP(this._session, path, mime, data, offset, size) 156 | } 157 | return streamP(this.chunkSize, chunkHandlerP) 158 | } 159 | 160 | async addAssetsToAlbumP(albumId, assets) { 161 | // assets is an array of { id: assetId, remoteId: remoteId } 162 | let albumAssets = assets.map((asset) => { 163 | let resource = { 164 | id: asset.id 165 | } 166 | if (asset.remoteId) { 167 | let importTimestamp = (new Date()).toISOString() 168 | resource.payload = { 169 | userCreated: importTimestamp, 170 | userUpdated: importTimestamp, 171 | publishInfo: { 172 | remoteId: asset.remoteId 173 | } 174 | } 175 | } 176 | return resource 177 | }) 178 | let path = `/v2/catalogs/${this._catalogId}/albums/${albumId}/assets` 179 | for (let len = albumAssets.length, i = 0; i < len; i += 50) { // maximum chunk size of 50 180 | let content = { resources: albumAssets.slice(i, i + 50) } 181 | let count = content.resources.length 182 | try { 183 | await LrRequestor.putP(this._session, path, content) 184 | } catch (err) { 185 | if (err.statusCode != 403) { 186 | throw err 187 | } 188 | // recover from error where all assets are already in the album; treat as success 189 | let response = err.error 190 | if (count != response.errors.filter((error) => error.subtype == 'ResourceExistsError').length) { 191 | throw err 192 | } 193 | console.log('add assets: all assets in chunk are already in album') 194 | } 195 | } 196 | } 197 | 198 | async existsP(path) { 199 | try { 200 | await LrRequestor.headP(this._session, path) 201 | return true 202 | } catch (err) { 203 | if (err.statusCode != 404) { 204 | throw err 205 | } 206 | return false 207 | } 208 | } 209 | 210 | async waitForP(path) { 211 | let sleep = () => new Promise(resolve => setTimeout(resolve, 3000)) // 3 sec 212 | let retries = 10 213 | for (let i = 0; i < retries; i++) { 214 | console.log(`try ${i}: sleeping...`) 215 | await sleep() 216 | console.log(`try ${i}: checking`) 217 | if (await this.existsP(path)) { 218 | console.log(`try ${i}: exists`) 219 | return path 220 | } 221 | } 222 | throw new Error(`timed out on: ${path}`) 223 | } 224 | 225 | getAssetRenditionP(assetId, type) { 226 | let path = `/v2/catalogs/${this._catalogId}/assets/${assetId}/renditions/${type}` 227 | return LrRequestor.getP(this._session, path) 228 | } 229 | 230 | waitForRenditionP(assetId, type) { 231 | let path = `/v2/catalogs/${this._catalogId}/assets/${assetId}/renditions/${type}` 232 | return this.waitForP(path) 233 | } 234 | 235 | assetRenditionExistsP(assetId, type) { 236 | let path = `/v2/catalogs/${this._catalogId}/assets/${assetId}/renditions/${type}` 237 | return this.existsP(path) 238 | } 239 | 240 | async generateRenditionP(assetId, type) { 241 | let path = `/v2/catalogs/${this._catalogId}/assets/${assetId}/renditions` 242 | let response = await LrRequestor.genP(this._session, path, type) 243 | 244 | let link = response.links[`/rels/rendition_type/${type}`] 245 | return `/v2/catalogs/${this._catalogId}/${link.href}` // return the path 246 | } 247 | } 248 | 249 | export default LrContext 250 | -------------------------------------------------------------------------------- /samples/src/common/lr/LrRequestor.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import RequestUtils from '../request/RequestUtils.mjs' 12 | 13 | let _toJSON = (body) => { 14 | if (body.byteLength === 0) { 15 | return // catch empty body to avoid an error in JSON parser 16 | } 17 | let decoder = new TextDecoder('utf-8') 18 | var string = decoder.decode(body) 19 | let while1Regex = /^while\s*\(\s*1\s*\)\s*{\s*}\s*/ // strip off while(1){} 20 | return JSON.parse(string.replace(while1Regex, '')) 21 | } 22 | 23 | let _isJSON = (contentType) => contentType == 'application/json' || contentType == 'application/json;charset=utf-8' 24 | 25 | let _onEnd = (res) => { 26 | if (res.status < 200 || res.status > 299) { 27 | throw { 28 | statusCode: res.status, 29 | error: _toJSON(res.body) 30 | } 31 | } 32 | return _isJSON(res.contentType) ? _toJSON(res.body) : res.body 33 | } 34 | 35 | let _unauthGetP = function(session, path, signal) { 36 | let headers = { 37 | 'X-API-Key': session.apiKey 38 | } 39 | let options = { 40 | method: 'GET', 41 | protocol: 'https:', 42 | host: session.host, 43 | path: path, 44 | headers: headers 45 | } 46 | return RequestUtils.requestP(options, undefined, signal).then(res => _onEnd(res)) 47 | } 48 | 49 | let _authRequestP = function(session, method, path, headers, data, signal) { 50 | headers = Object.assign({}, headers) 51 | headers['X-API-Key'] = session.apiKey 52 | headers['Authorization'] = `Bearer ${session.accessToken}` 53 | let options = { 54 | method: method, 55 | protocol: 'https:', 56 | host: session.host, 57 | path: path, 58 | headers: headers 59 | } 60 | return RequestUtils.requestP(options, data, signal).then(res => _onEnd(res)) 61 | } 62 | 63 | let _getPagedP = async function(session, path, resources = []) { 64 | let res = await _authRequestP(session, 'GET', path, {}) 65 | res.resources = resources.concat(res.resources) 66 | if (res.links && res.links.next) { 67 | let uri = `${res.base}${res.links.next.href}` 68 | let url = new URL(uri) // peel off the path of the fully formed uri 69 | return _getPagedP(session, url.pathname + url.search, res.resources) 70 | } 71 | return res 72 | } 73 | 74 | let _sendJSONP = function(session, method, path, json, sha256) { 75 | let headers = { 'Content-Type': 'application/json' } 76 | if (sha256) { 77 | headers['If-None-Match'] = sha256 78 | } 79 | return _authRequestP(session, method, path, headers, JSON.stringify(json)) 80 | } 81 | 82 | let _putChunkP = function(session, path, type, data, offset, size) { 83 | let headers = { 'Content-Type': type } 84 | let length = data.byteLength 85 | if (size != length) { 86 | headers['Content-Range'] = `bytes ${offset}-${offset + length - 1}/${size}` 87 | } 88 | return _authRequestP(session, 'PUT', path, headers, data) 89 | } 90 | 91 | let LrRequestor = { 92 | healthP: (apiKey, host = 'lr.adobe.io') => _unauthGetP({ apiKey, host }, '/v2/health'), 93 | headP: (session, path) => _authRequestP(session, 'HEAD', path, {}), 94 | getP: (session, path) => _authRequestP(session, 'GET', path, {}), 95 | getPagedP: (session, path) => _getPagedP(session, path), 96 | putP: (session, path, json) => _sendJSONP(session, 'PUT', path, json), 97 | putUniqueP: (session, path, json, sha256) => _sendJSONP(session, 'PUT', path, json, sha256), 98 | putChunkP: (session, path, type, data, offset, size) => _putChunkP(session, path, type, data, offset, size), 99 | postP: (session, path, json) => _sendJSONP(session, 'POST', path, json), 100 | deleteP: (session, path) => _authRequestP(session, 'DELETE', path, {}), 101 | genP: (session, path, type) => _authRequestP(session, 'POST', path, { 'X-Generate-Renditions': type }) 102 | } 103 | 104 | export default LrRequestor 105 | -------------------------------------------------------------------------------- /samples/src/common/lr/LrSession.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrAuth from './LrAuth.mjs' 12 | import LrRequestor from './LrRequestor.mjs' 13 | import LrContext from './LrContext.mjs' 14 | 15 | let _lr 16 | 17 | let _fetchAccountP = async (session) => { 18 | try { 19 | return await LrRequestor.getP(session, '/v2/account') 20 | } 21 | catch (err) { 22 | throw new Error('error: failed to get account') // recast 23 | } 24 | } 25 | 26 | let _getEntitledAccountP = async (session) => { 27 | let account = await _fetchAccountP(session) 28 | let status = account.entitlement.status 29 | if (status != 'trial' && status != 'subscriber') { 30 | throw new Error('error: user not entitled') 31 | } 32 | return account 33 | } 34 | 35 | let _fetchCatalogP = async (session) => { 36 | try { 37 | return await LrRequestor.getP(session, '/v2/catalog') 38 | } 39 | catch (err) { 40 | throw new Error('error: failed to get catalog') // recast 41 | } 42 | } 43 | 44 | let _getExistingCatalogP = async (session) => { 45 | let catalog = await _fetchCatalogP(session) 46 | if (!catalog) { 47 | throw new Error('error: user has no catalog') 48 | } 49 | return catalog 50 | } 51 | 52 | let LrSession = { 53 | currentContextP: async () => { 54 | if (_lr) { 55 | return _lr 56 | } 57 | let session = await LrAuth.authenticateP() 58 | let account = await _getEntitledAccountP(session) 59 | let catalog = await _getExistingCatalogP(session) 60 | _lr = new LrContext(session, account, catalog) 61 | return _lr 62 | } 63 | } 64 | 65 | export default LrSession 66 | -------------------------------------------------------------------------------- /samples/src/common/lr/LrUtils.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | let LrUtils = { 12 | getAssetAspectRatio: (asset) => { 13 | if (asset.payload.develop) { 14 | let width = asset.payload.develop.croppedWidth 15 | let height = asset.payload.develop.croppedHeight 16 | if (width && height) { 17 | return width / height 18 | } 19 | } 20 | else { 21 | let width = asset.payload.importSource.originalWidth 22 | let height = asset.payload.importSource.originalHeight 23 | if (width && height) { 24 | return width / height 25 | } 26 | } 27 | return 4 / 3 28 | }, 29 | 30 | logAlbumAssetsP: async (lr, album, offset = '') => { 31 | let albumAssets = await lr.getAlbumAssetsP(album.id) 32 | albumAssets.forEach((albumAsset) => { 33 | let assetId = albumAsset.asset.id 34 | let remoteId = albumAsset.payload.publishInfo ? albumAsset.payload.publishInfo.remoteId : undefined 35 | console.log(`${offset}{ id: ${assetId}, remoteId: ${remoteId} }`) 36 | }) 37 | }, 38 | 39 | logAlbumP: async (lr, album, offset = '') => { 40 | let name = album.payload.name 41 | let remoteId = album.payload.publishInfo ? album.payload.publishInfo.remoteId : undefined 42 | console.log(`${offset}${album.subtype} { id: ${album.id}, name: ${name}, remoteId: ${remoteId} }`) 43 | if (album.subtype == 'project') { 44 | await LrUtils.logAlbumAssetsP(lr, album, offset + ' ') 45 | } 46 | }, 47 | 48 | logAlbumHierarchyP: async (lr, node, offset = '') => { 49 | if (node.data) { 50 | await LrUtils.logAlbumP(lr, node.data, offset) 51 | } 52 | else { 53 | console.log('root') 54 | } 55 | if (node.folders || node.albums) { 56 | for (const inode of node.folders) { 57 | await LrUtils.logAlbumHierarchyP(lr, inode, offset + ' ') 58 | } 59 | for (const inode of node.albums) { 60 | await LrUtils.logAlbumHierarchyP(lr, inode, offset + ' ') 61 | } 62 | } 63 | }, 64 | 65 | createAlbumHierarchy: (subtype, name, albums) => { 66 | let root = { 67 | name, 68 | folders: [], 69 | albums: [] 70 | } 71 | 72 | albums.sort((a, b) => { // alphabetical sort, with sets first 73 | if (a.subtype != b.subtype) { 74 | return a.subtype > b.subtype ? -1 : 1 75 | } 76 | return a.payload.name < b.payload.name ? -1 : 1 77 | }) 78 | 79 | let inodeHash = {} 80 | albums.forEach((album) => { 81 | if (album.subtype == subtype) { 82 | inodeHash[album.id] = { 83 | name: album.payload.name, 84 | folders: [], 85 | albums: [], 86 | data: album 87 | } 88 | } 89 | }) 90 | 91 | albums.forEach((album) => { 92 | let parent = (album.payload.parent && inodeHash[album.payload.parent.id]) || root 93 | let inode = inodeHash[album.id] 94 | if (inode) { 95 | inode.parent = parent 96 | parent.folders.push(inode) 97 | } 98 | else { 99 | parent.albums.push({ 100 | parent, 101 | name: album.payload.name, 102 | folders: [], 103 | albums: [], 104 | data: album 105 | }) 106 | } 107 | }) 108 | 109 | return root 110 | }, 111 | 112 | getRootCollectionSetP: async (lr) => { 113 | let albums = await lr.getAlbumsP('collection_set%3Bcollection') 114 | return LrUtils.createAlbumHierarchy('collection_set', 'Albums', albums) 115 | }, 116 | 117 | getRootProjectSetP: async (lr) => { 118 | let albums = await lr.getAlbumsP('project_set%3Bproject') 119 | return LrUtils.createAlbumHierarchy('project_set', 'Projects', albums) 120 | } 121 | } 122 | 123 | export default LrUtils 124 | -------------------------------------------------------------------------------- /samples/src/common/original/OriginalUtils.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import CryptoUtils from '../crypto/CryptoUtils.mjs' 12 | 13 | let _subtypeGuess = function(fileext) { 14 | let videoExts = [ 15 | '.mp4', 16 | '.MP4', 17 | '.mov', 18 | '.MOV', 19 | '.avi', 20 | '.AVI', 21 | '.mpg', 22 | '.MPG', 23 | '.m4v', 24 | '.M4V' 25 | ] 26 | return videoExts.find((ext) => ext == fileext) ? 'video' : 'image' 27 | } 28 | 29 | let OriginalUtils = { 30 | create: (path, parent, name, size, streamP) => { 31 | let index = name.lastIndexOf('.') 32 | let ext = (index < 1 || index === 0) ? '' : name.substring(index) // emulate node path.extname 33 | let subtype = _subtypeGuess(ext) 34 | let mime = subtype == 'video' ? 'application/octet-stream;video' : 'application/octet-stream' // or 'video/*' 35 | return { 36 | model: { 37 | path, 38 | parent, 39 | name, 40 | ext, 41 | mime, 42 | subtype, 43 | size, 44 | }, 45 | streamP 46 | } 47 | }, 48 | 49 | uploadP: async (lr, orig) => { 50 | let model = orig.model 51 | 52 | // compute the sha256 to check if there are duplicates 53 | let sha256 = await CryptoUtils.sha256P(orig.streamP) 54 | 55 | // create a new asset; if there is a duplicate, return the duplicate asset id 56 | let assetId 57 | try { 58 | assetId = await lr.createRevisionP(model.subtype, model.name, model.size, sha256) 59 | } catch(err) { 60 | if (err.statusCode != 412) { 61 | throw err 62 | } 63 | console.log('skipped upload (returning first duplicate)') 64 | let revision = err.error.revisions[0] 65 | return revision.links['/rels/asset'].href.match(/assets\/([a-f0-9]{32})\/?/)[1] 66 | } 67 | 68 | // upload the bits; if there is a failure, the asset is 'incomplete' 69 | try { 70 | await lr.putOriginalP(assetId, model.mime, model.size, orig.streamP) 71 | } 72 | catch (err) { 73 | console.log(`error on upload left an incomplete asset: ${assetId}`) // should retry 74 | throw(err) 75 | } 76 | return assetId 77 | } 78 | } 79 | 80 | export default OriginalUtils 81 | -------------------------------------------------------------------------------- /samples/src/common/request/RequestUtils.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | let RequestUtils = {} 12 | 13 | let node 14 | try { node = process.versions.node } catch (err) {} 15 | 16 | if (node) { 17 | // import https from 'https' // could use static import if we are only on node 18 | // import http from 'http' // could use static import if we are only on node 19 | let _httpsPromise = import('https') 20 | let _httpPromise = import('http') 21 | 22 | let _requestP = (module, options, data, signal) => new Promise((resolve, reject) => { 23 | if (data) { 24 | options.headers['content-length'] = Buffer.byteLength(data) 25 | } 26 | let req = module.request(options, (res) => { 27 | let chunks = [] 28 | res.on('data', (chunk) => chunks.push(Buffer.from(chunk))) 29 | res.on('end', () => resolve({ 30 | status: res.statusCode, 31 | contentType: res.headers['content-type'], 32 | body: Buffer.concat(chunks) 33 | })) 34 | }).on('error', reject).end(data) 35 | 36 | if (signal) { 37 | signal.onabort = () => req.destroy(new Error('request aborted')) 38 | } 39 | }) 40 | 41 | RequestUtils.requestP = (options, data, signal) => { 42 | let modulePromise = options.protocol === 'http:' ? _httpPromise : _httpsPromise 43 | return modulePromise.then(module => _requestP(module, options, data, signal)) 44 | } 45 | } 46 | else { 47 | let _fetchP = (options, data, signal) => new Promise((resolve, reject) => { 48 | if (options.method !== 'GET') { 49 | reject('only gets for now') 50 | return 51 | } 52 | let port = options.port ? `:${options.port}` : '' 53 | let url = `${options.protocol}//${options.host}${port}${options.path}` 54 | options = { 55 | method: options.method, 56 | headers: options.headers, 57 | signal 58 | } 59 | fetch(url, options).then(res => { 60 | res.arrayBuffer().then(body => resolve({ 61 | status: res.status, 62 | contentType: res.headers.get('content-type'), 63 | body 64 | })) 65 | }).catch(reject) 66 | }) 67 | 68 | let _xhrP = (options, data, signal) => new Promise((resolve, reject) => { 69 | let blob = data ? new Blob([data]) : undefined 70 | let port = options.port ? `:${options.port}` : '' 71 | let url = `${options.protocol}//${options.host}${port}${options.path}` 72 | let xhr = new XMLHttpRequest() 73 | xhr.open(options.method, url, true) 74 | Object.entries(options.headers).forEach(([key, value]) => xhr.setRequestHeader(key, value)) 75 | xhr.responseType = 'arraybuffer' 76 | xhr.onload = () => resolve({ 77 | status: xhr.status, 78 | contentType: xhr.getResponseHeader('content-type'), 79 | body: xhr.response 80 | }) 81 | xhr.onabort = () => reject(new Error('xhr aborted')) 82 | xhr.onerror = () => reject(new Error('xhr failed')) 83 | xhr.send(blob) 84 | 85 | if (signal) { 86 | signal.onabort = () => xhr.abort() 87 | } 88 | }) 89 | 90 | RequestUtils.fetchP = _fetchP 91 | RequestUtils.xhrP = _xhrP 92 | RequestUtils.requestP = _xhrP // default 93 | } 94 | 95 | export default RequestUtils 96 | -------------------------------------------------------------------------------- /samples/src/node/.gitignore: -------------------------------------------------------------------------------- 1 | media/ 2 | prod.sh 3 | stage.sh 4 | -------------------------------------------------------------------------------- /samples/src/node/README.md: -------------------------------------------------------------------------------- 1 | ## Node Samples 2 | 3 | Tested with Node.js 14.15.0. 4 | 5 | --- 6 | 7 | This directory contains various examples using the Lightroom Services APIs in a Node.js application. 8 | 9 | ### Prerequisites 10 | 11 | * Developers must create an application integration with the `Lightroom Services` as described in the [Adobe Lightroom developer documentation](https://developer.adobe.com/lightroom). This process will generate an `API Key` and `Client Secret`. 12 | 13 | * Developers must acquire an OAuth 2.0 `user access token` for a Lightroom customer as detailed in the [Adobe Developer authentication documentation](https://developer.adobe.com/authentication/auth-methods.html#!AdobeDocs/adobeio-auth/master/OAuth/OAuth.md), with the `openid`, `lr_partner_apis` and `lr_partner_rendition_apis` scopes. 14 | 15 | * The samples acquire the API Key and user access token through the 16 | `process.env.KEY` and `process.env.TOKEN` environment variables. These can be passed into the application on the command line: 17 | 18 | KEY='' TOKEN='' node 19 | 20 | ### Common Code 21 | 22 | Code shared by the Node.js examples can be found in the `common` sub-directory. Common file system utilities can be found in `common/file`. In particular, `common/file/File.mjs` leverages the `../common/original/OriginalUtils.mjs` media abstraction to support uploading files to Lightroom from the file system. 23 | 24 | ### Samples 25 | 26 | The samples are divided into different sub-directories of related kind. They leverage the modules found in the `../common` sibling directory for most of their Lightroom operations. 27 | 28 | * Health 29 | 30 | * health/gethealth.mjs 31 | Query the health endpoint of the Lightroom Services and print the result the console. 32 | 33 | * Get Content 34 | 35 | * get/getalbum.mjs 36 | Fetch the album with the given identifier and print the result to the console. 37 | 38 | * get/getalbumassets.mjs 39 | Fetch the album assets of the album with the given identifier and print the result to the console. 40 | 41 | * get/getalbumcover.mjs 42 | Fetch the album cover of the album with the given identifier and print the result to the console. 43 | 44 | * get/getasset.mjs 45 | Fetch the asset with the given identifier and print the result to the console. 46 | 47 | * get/getasset2048.mjs 48 | Fetch the 2048 rendition of the asset with the given identifier and output the result to the file `.2048.jpg`. 49 | 50 | * get/getassetthumb.mjs 51 | Fetch the thumbnail rendition of the asset with the given identifier and output the result to the file `.thumb.jpg`. 52 | 53 | * get/getcollections.mjs 54 | Fetch all albums of subtype `collection_set` and `collection` and print the result to the console. 55 | 56 | * get/getfirstalbumasset.mjs 57 | Fetch the first album asset of the album with the given identifier and print the result to the console. 58 | 59 | * get/getfirstasset.mjs 60 | Fetch the first asset in the catalog and print the result to the console. 61 | 62 | * get/getincompletes.mjs 63 | Fetch all incomplete assets (assets that have been created but do not have a corresponding original or proxy) and print the result to the console. 64 | 65 | * Upload Assets 66 | 67 | * upload/uploadfile.mjs 68 | Create a new asset and upload the file as its original. Skip the operation if a duplicate asset is detected. 69 | 70 | * Connect Content 71 | 72 | * connect/connectdir.js 73 | Note: The sample assumes that the directory contains only media files and no subdirectories or non-media files. 74 | 75 | Upload all media files in the given directory, then create a new project album sharing the name of the directory and add all of the media files to the project album. Create a parent `project_set` of the new project album, named with a timestamp. Prints the result of the hierarchy to the console. 76 | 77 | If duplicates are found for any of the media files, the upload is skipped, and the existing duplicate asset is added to the project album instead. 78 | 79 | * connect/createproject.js 80 | Fetch all albums of subtype `project_set` and `project`, as well as the album assets of projects, and print the hierarchy to the console. 81 | 82 | * connect/deletealbum.js 83 | Delete an albums of subtype `project_set` or `project`, and print the result to the console. 84 | 85 | * get/getprojects.mjs 86 | Fetch all albums of subtype `project_set` and `project` and print the result to the console. 87 | 88 | * connect/logconnector.js 89 | Fetch all albums of subtype `project_set` and `project`, as well as the album assets of projects, and print the hierarchy to the console. 90 | 91 | * Generate Renditions 92 | 93 | * gen/gen2560.mjs 94 | Generate 2560 rendition for the asset with the given identifier. 95 | 96 | * gen/genfullsize.mjs 97 | Generate fullsize rendition for an asset with the given identifier. 98 | 99 | * gen/getasset2560.mjs 100 | Fetch the 2560 rendition of the asset with the given identifier and output the result to the file `.2560.jpg`. 101 | 102 | * gen/getassetfullsize.mjs 103 | Fetch the fullsize rendition of the asset with the given identifier and output the result to the file `.fullsize.jpg`. 104 | 105 | * gen/renditionsexist.mjs 106 | Print whether the asset with the given identifier has a valid 2560 rendition and whether it has a valid fullsize rendition. 107 | 108 | * gen/waitfor2560.mjs 109 | Wait for the 2560 rendition of the asset with the given identifier to be available. It will poll up to ten times, once every three seconds before timing out. 110 | 111 | * gen/waitforfullsize.mjs 112 | Wait for the fullsize rendition of the asset with the given identifier to be available. It will poll up to ten times, once every three seconds before timing out. 113 | 114 | * Expected Errors 115 | 116 | * err/abort.mjs 117 | Check that aborting an HTTP request returns the expected error. To run the sample, it is necessary to first launch the `common/proxy/ProxyDelayHTTP.js` server, which listens on `http://localhost:8000`. This server inserts a three second delay between a request and its response, giving time for an abort signal to be triggered. 118 | 119 | * err/apikey.mjs 120 | Tests that an invalid API key throws the expected error, on a request to the Lightroom Services APIs. 121 | -------------------------------------------------------------------------------- /samples/src/node/common/file/Directory.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import fs from 'fs' 12 | import path from 'path' 13 | import File from './File.mjs' 14 | import OriginalUtils from '../../../common/original/OriginalUtils.mjs' 15 | 16 | let Directory = { 17 | filesP: async function(dirPath) { 18 | let entries = await fs.promises.readdir(dirPath, { withFileTypes: true }) 19 | let files = entries.filter((entry) => !entry.isDirectory()) 20 | files = files.filter((file) => !(/^\./).test(file.name)) // skip hidden 21 | return Promise.all(files.map((file) => File.originalP(path.join(dirPath, file.name)))) 22 | }, 23 | 24 | uploadFilesP: async function(lr, origs) { 25 | let assets = [] 26 | for (const orig of origs) { 27 | let assetId = await OriginalUtils.uploadP(lr, orig) // could be parallel 28 | assets.push( { id: assetId, remoteId: orig.model.path }) 29 | } 30 | return assets 31 | } 32 | } 33 | 34 | export default Directory 35 | -------------------------------------------------------------------------------- /samples/src/node/common/file/File.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import fs from 'fs' 12 | import path from 'path' 13 | import OriginalUtils from '../../../common/original/OriginalUtils.mjs' 14 | 15 | let File = { 16 | streamP: (filePath, chunkSize, chunkHandlerP) => new Promise((resolve, reject) => { 17 | let offset = 0 18 | async function readableHandler() { 19 | let data 20 | while (null !== (data = stream.read(chunkSize))) { 21 | try { 22 | await chunkHandlerP(data, offset) 23 | offset += data.length 24 | } catch (err) { 25 | stream.destroy(err) 26 | } 27 | } 28 | stream.once('readable', readableHandler) 29 | } 30 | const stream = fs.createReadStream(filePath, { highWaterMark: chunkSize }) 31 | stream.on('end', resolve) 32 | stream.on('error', reject) 33 | stream.once('readable', readableHandler) 34 | }), 35 | 36 | streamController: (filePath) => { 37 | return (chunkSize, chunkHandlerP) => File.streamP(filePath, chunkSize, chunkHandlerP) 38 | }, 39 | 40 | originalP: async function(filePath) { 41 | let stats = await fs.promises.stat(filePath) 42 | let parent = path.dirname(filePath) 43 | let name = path.basename(filePath) 44 | return OriginalUtils.create(filePath, parent, name, stats.size, File.streamController(filePath)) 45 | } 46 | } 47 | 48 | export default File 49 | -------------------------------------------------------------------------------- /samples/src/node/common/file/FileUtils.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import fs from 'fs' 12 | 13 | let FileUtils = { 14 | readStringFromFileP: (name) => new Promise((resolve, reject) => { 15 | fs.readFile(name, 'utf8', function(err, data) { 16 | if(err) { 17 | reject(err) 18 | } else { 19 | resolve(data) 20 | } 21 | }) 22 | }), 23 | 24 | writeStringToFileP: (buffer, name) => new Promise((resolve, reject) => { 25 | fs.writeFile(name, buffer, 'utf8', function(err) { 26 | if(err) { 27 | reject(err) 28 | } else { 29 | resolve() 30 | } 31 | }) 32 | }), 33 | 34 | writeBufferToFileP: (buffer, name) => new Promise((resolve, reject) => { 35 | fs.writeFile(name, buffer, function(err) { 36 | if(err) { 37 | reject(err) 38 | } else { 39 | resolve() 40 | } 41 | }) 42 | }) 43 | } 44 | 45 | export default FileUtils 46 | -------------------------------------------------------------------------------- /samples/src/node/common/proxy/delay/ProxyDelayHTTP.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | const https = require('https') 12 | const http = require('http') 13 | 14 | class Deferred { 15 | constructor() { 16 | this.promise = new Promise((resolve, reject) => this._deferredApi = { resolve, reject }) 17 | } 18 | 19 | resolve(value) { 20 | this._deferredApi.resolve(value) 21 | } 22 | 23 | reject(value) { 24 | this._deferredApi.reject(value) 25 | } 26 | } 27 | 28 | let _requestListener = (host) => (req, res) => { 29 | let headers = Object.assign({}, req.headers) 30 | delete headers.host 31 | let options = { 32 | method: req.method, 33 | protocol: 'https:', 34 | host, 35 | path: req.url, 36 | headers 37 | } 38 | let proxy_req = https.request(options, (proxy_res) => { 39 | let deferred = new Deferred() 40 | setTimeout(() => deferred.resolve(), 3000) // defer response for three seconds 41 | res.writeHead(proxy_res.statusCode, proxy_res.headers) 42 | proxy_res.on('data', (chunk) => deferred.promise.then(() => res.write(chunk))) 43 | proxy_res.on('end', () => deferred.promise.then(() => res.end())) 44 | }).on('error', (err) => console.log('server error', err)) 45 | req.on('abort', (arg) => console.log('server abort', arg)) 46 | req.on('data', chunk => proxy_req.write(chunk)) 47 | req.on('end', () => proxy_req.end()) 48 | } 49 | 50 | const hostname = 'localhost' 51 | const port = 8000 52 | const remote = process.env.HOST || 'lr.adobe.io' 53 | 54 | http.createServer({}, _requestListener(remote)) 55 | .listen(port, hostname, (err) => { 56 | if (err) console.log('listen error:', err) 57 | console.log(`listening on: http://${hostname}:${port}`) 58 | }) 59 | -------------------------------------------------------------------------------- /samples/src/node/connect/connectdir.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | import LrUtils from '../../common/lr/LrUtils.mjs' 13 | import Directory from '../common/file/Directory.mjs' 14 | import path from 'path' 15 | 16 | async function _projectFromDirP(lr, dirPath, parentId) { 17 | let origs = await Directory.filesP(dirPath) 18 | let assets = await Directory.uploadFilesP(lr, origs) 19 | let name = path.basename(dirPath) // name project the same as the directory 20 | let projectId = await lr.createAlbumP('project', name, parentId, `${name}.remoteId`) 21 | await lr.addAssetsToAlbumP(projectId, assets) 22 | return projectId 23 | } 24 | 25 | async function mainP(dirPath) { 26 | if (!dirPath) { 27 | console.log('usage: connectdir ') 28 | return 29 | } 30 | let lr = await LrSession.currentContextP() 31 | 32 | // create a project set to hold the project; named with a timestamp 33 | let rootName = new Date().toISOString() 34 | let parentId = await lr.createAlbumP('project_set', rootName, null, `${rootName}.remoteId`) 35 | let projectId = await _projectFromDirP(lr, dirPath, parentId) 36 | 37 | // print out the results 38 | console.log(`created project set: { id: ${parentId}, name: ${rootName} }`) 39 | let project = await lr.getAlbumP(projectId) 40 | await LrUtils.logAlbumP(lr, project) 41 | } 42 | 43 | let dirPath = process.argv[2] 44 | mainP(dirPath).then(() => console.log('done')).catch(e => console.error(e)) 45 | -------------------------------------------------------------------------------- /samples/src/node/connect/createproject.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | 13 | async function mainP(name, remoteId) { 14 | if (!name || !remoteId) { 15 | console.log('usage: createproject ') 16 | return 17 | } 18 | let lr = await LrSession.currentContextP() 19 | let albumId = await lr.createAlbumP('project', name, null, remoteId) 20 | console.log(`created project: ${albumId}`) 21 | } 22 | 23 | mainP(process.argv[2], process.argv[3]).then(() => console.log('done')).catch(e => console.error('error:', e)) 24 | -------------------------------------------------------------------------------- /samples/src/node/connect/deletealbum.mjs: -------------------------------------------------------------------------------- 1 | import LrSession from '../../common/lr/LrSession.mjs' 2 | 3 | async function mainP(albumId) { 4 | let lr = await LrSession.currentContextP() 5 | await lr.deleteAlbumP(albumId) 6 | console.log('deleted album', albumId) 7 | } 8 | 9 | let albumId = process.argv[2] 10 | mainP(albumId).then(() => console.log('done')).catch(e => console.error(e)) 11 | -------------------------------------------------------------------------------- /samples/src/node/connect/getprojects.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | 13 | async function mainP() { 14 | let lr = await LrSession.currentContextP() 15 | let response = await lr.getAlbumsP('project_set%3Bproject') 16 | console.log(JSON.stringify(response, null, 2)) 17 | } 18 | 19 | mainP().then(() => console.log('done')).catch(e => console.error('error:', e)) 20 | -------------------------------------------------------------------------------- /samples/src/node/connect/logconnector.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | import LrUtils from '../../common/lr/LrUtils.mjs' 13 | 14 | async function mainP() { 15 | let lr = await LrSession.currentContextP() 16 | let root = await LrUtils.getRootProjectSetP(lr) 17 | await LrUtils.logAlbumHierarchyP(lr, root) 18 | } 19 | 20 | mainP().then(() => console.log('done')).catch(e => console.error('error:', e)) 21 | -------------------------------------------------------------------------------- /samples/src/node/err/abort.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrAuth from '../../common/lr/LrAuth.mjs' 12 | import RequestUtils from '../../common/request/RequestUtils.mjs' 13 | 14 | let _abortHealthP = function(apiKey, delay) { 15 | let headers = { 16 | 'X-API-Key': apiKey, 17 | 'Cache-Control': 'no-cache, no-store, must-revalidate' 18 | } 19 | let options = { 20 | method: 'GET', 21 | protocol: 'http:', 22 | host: 'localhost', 23 | port: 8000, 24 | path: '/v2/health', 25 | headers 26 | } 27 | let signal = { 28 | onabort: () => console.log('uncaught abort') 29 | } 30 | let promise = RequestUtils.requestP(options, undefined, signal) 31 | setTimeout(() => { 32 | signal.onabort() 33 | }, delay) 34 | return promise 35 | } 36 | 37 | async function mainP() { 38 | console.log('start test...') 39 | try { 40 | let health = await _abortHealthP(LrAuth.getApiKey(), 10000) 41 | console.log('expecting health:', health) 42 | } 43 | catch (err) { 44 | console.log('unexpected error', err) 45 | } 46 | 47 | try { 48 | let health = await _abortHealthP(LrAuth.getApiKey(), 1000) 49 | console.log('unexpected health:', health) 50 | } 51 | catch (err) { 52 | console.log('expecting error (aborted)', err) 53 | } 54 | 55 | try { 56 | let health = await _abortHealthP(LrAuth.getApiKey(), 6000) 57 | console.log('expecting health:', health) 58 | } 59 | catch (err) { 60 | console.log('unexpected error', err) 61 | } 62 | console.log('test done.') 63 | } 64 | 65 | mainP() 66 | -------------------------------------------------------------------------------- /samples/src/node/err/apikey.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrAuth from '../../common/lr/LrAuth.mjs' 12 | import LrRequestor from '../../common/lr/LrRequestor.mjs' 13 | 14 | async function mainP() { 15 | try { 16 | let response = await LrRequestor.healthP('InvalidApiKey', LrAuth.getHost()) 17 | console.log('unexpected success:', JSON.stringify(response, null, 2)) 18 | } 19 | catch (err) { 20 | console.log('expected error (api key is invalid):', err) 21 | } 22 | } 23 | 24 | mainP() 25 | -------------------------------------------------------------------------------- /samples/src/node/gen/README.md: -------------------------------------------------------------------------------- 1 | ## Lightroom Originals, Proxies, and Renditions 2 | 3 | Lightroom is built on a non-destructive editing model, where the original media (raw image, jpeg, or other supported type) is left untouched, and edits are held in a sidecar data structure affiliated with the asset. Whenever a rendition is required, Adobe Camera Raw is employed to generate jpeg renditions of various resolutions from the original and its edit metadata. 4 | 5 | To optimize workflows when the original is not readily available or is too resource heavy, Lightroom includes a `proxy DNG`, sometimes referred to as a `smart preview`, that can stand in for the original file. The proxy has a maximum dimension of 2560 pixels on a side. As with the original, Adobe Camera Raw can apply the asset edit metadata to the proxy to generate jpeg renditions of various resolutions. 6 | 7 | _NOTE: Lightroom (on desktop, web, and mobile) always uploads the original file and proxy for every asset. However, Lightroom Classic will only upload a proxy for assets originating from Lightroom Classic._ 8 | 9 | ### Cached Renditions 10 | 11 | Lightroom always generates and caches two renditions for each asset: a thumbnail rendition (`thumbnail2x`) and a screen size rendition with a maximum dimension of 2048 pixels on a side (`2048`). These are both available immediately through the Lightroom Services APIs. 12 | 13 | ### Generating Renditions 14 | 15 | Workflows that require renditions of higher resolution than the cached 2048 can use the Lightroom Services APIs to generate `2560` (from the proxy and having its dimensions) or `fullsize` (from the original and sharing its dimensions) renditions on demand. 16 | 17 | Rendition generation is an asynchronous process that might take several seconds to complete, depending on the dimensions of the original and the sophistication of the edits. A client calls an endpoint and receives a rendition `link` with the the URL at which the rendition will eventually be available. It then polls, through a `HEAD` call, until the rendition exists, after which it can fetch the result. 18 | 19 | There are some important considerations when generating renditions: 20 | 21 | * Lightroom Classic only uploads the 2560 proxy and has no original. Therefore, it is not possible to generate a fullsize rendition, and attempting to do so will result in an error. A client should first check if the original `link` is present in the asset. If not, it should request that a 2560 rendition be generated instead. 22 | 23 | * The 2560 or fullsize renditions are purged after an unspecified time. Therefore, clients should check whether the rendition exists to determine if they need to request that a new rendition be generated. 24 | -------------------------------------------------------------------------------- /samples/src/node/gen/gen2560.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | 13 | async function mainP(assetId) { 14 | let lr = await LrSession.currentContextP() 15 | let path = await lr.generateRenditionP(assetId, '2560') 16 | let response = await lr.waitForRenditionP(assetId, '2560') 17 | console.log(JSON.stringify(response, null, 2)) 18 | } 19 | 20 | mainP(process.argv[2]).then(() => console.log('done')).catch(e => console.error('error:', e)) 21 | -------------------------------------------------------------------------------- /samples/src/node/gen/genfullsize.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | 13 | async function mainP(assetId) { 14 | let lr = await LrSession.currentContextP() 15 | let path = await lr.generateRenditionP(assetId, 'fullsize') 16 | let response = await lr.waitForRenditionP(assetId, 'fullsize') 17 | console.log(JSON.stringify(response, null, 2)) 18 | } 19 | 20 | mainP(process.argv[2]).then(() => console.log('done')).catch(e => console.error('error:', e)) 21 | -------------------------------------------------------------------------------- /samples/src/node/gen/getasset2560.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | import FileUtils from '../../common/file/FileUtils.mjs' 13 | 14 | async function mainP(assetId) { 15 | let lr = await LrSession.currentContextP() 16 | let buffer = await lr.getAssetRenditionP(assetId, '2560') 17 | let name = `${assetId}.2560.jpg` 18 | await FileUtils.writeBufferToFileP(buffer, name) 19 | console.log('success: ', name) 20 | } 21 | 22 | mainP(process.argv[2]).then(() => console.log('done')).catch(e => console.error('error:', e)) 23 | -------------------------------------------------------------------------------- /samples/src/node/gen/getassetfullsize.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | import FileUtils from '../../common/file/FileUtils.mjs' 13 | 14 | async function mainP(assetId) { 15 | let lr = await LrSession.currentContextP() 16 | let buffer = await lr.getAssetRenditionP(assetId, 'fullsize') 17 | let name = `${assetId}.fullsize.jpg` 18 | await FileUtils.writeBufferToFileP(buffer, name) 19 | console.log('success: ', name) 20 | } 21 | 22 | mainP(process.argv[2]).then(() => console.log('done')).catch(e => console.error('error:', e)) 23 | -------------------------------------------------------------------------------- /samples/src/node/gen/renditionsexist.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | 13 | async function mainP(assetId) { 14 | let lr = await LrSession.currentContextP() 15 | console.log('2560 exists:', await lr.assetRenditionExistsP(assetId, '2560')) 16 | console.log('fullsize exists:', await lr.assetRenditionExistsP(assetId, 'fullsize')) 17 | } 18 | 19 | mainP(process.argv[2]).then(() => console.log('done')).catch(e => console.error('error:', e)) 20 | -------------------------------------------------------------------------------- /samples/src/node/gen/waitfor2560.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | 13 | async function mainP(assetId) { 14 | let lr = await LrSession.currentContextP() 15 | let result = await lr.waitForRenditionP(assetId, '2560') 16 | console.log('result:', result) 17 | } 18 | 19 | mainP(process.argv[2]).then(() => console.log('done')).catch(e => console.error('error:', e)) 20 | -------------------------------------------------------------------------------- /samples/src/node/gen/waitforfullsize.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | 13 | async function mainP(assetId) { 14 | let lr = await LrSession.currentContextP() 15 | let result = await lr.waitForRenditionP(assetId, 'fullsize') 16 | console.log('result:', result) 17 | } 18 | 19 | mainP(process.argv[2]).then(() => console.log('done')).catch(e => console.error('error:', e)) 20 | -------------------------------------------------------------------------------- /samples/src/node/get/getalbum.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | 13 | async function mainP(albumId) { 14 | let lr = await LrSession.currentContextP() 15 | let album = await lr.getAlbumP(albumId) 16 | console.log(JSON.stringify(album, null, 2)) 17 | } 18 | 19 | mainP(process.argv[2]).then(() => console.log('done')).catch(e => console.error('error:', e)) 20 | -------------------------------------------------------------------------------- /samples/src/node/get/getalbumassets.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | 13 | async function mainP(albumId) { 14 | let lr = await LrSession.currentContextP() 15 | let response = await lr.getAlbumAssetsP(albumId) 16 | console.log(JSON.stringify(response, null, 2)) 17 | } 18 | 19 | mainP(process.argv[2]).then(() => console.log('done')).catch(e => console.error('error:', e)) 20 | -------------------------------------------------------------------------------- /samples/src/node/get/getalbumcover.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | import FileUtils from '../../common/file/FileUtils.mjs' 13 | 14 | async function mainP(albumId) { 15 | let lr = await LrSession.currentContextP() 16 | let album = await lr.getAlbumP(albumId) 17 | let buffer = await lr.getAlbumCoverP(album) 18 | if (buffer) { 19 | let name = `${albumId}.thumb.jpg` 20 | await FileUtils.writeBufferToFileP(buffer, name) 21 | console.log('success: ', name) 22 | } 23 | else { 24 | console.log('success: no album cover') 25 | } 26 | } 27 | 28 | mainP(process.argv[2]).then(() => console.log('done')).catch(e => console.error('error:', e)) 29 | -------------------------------------------------------------------------------- /samples/src/node/get/getasset.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | 13 | async function mainP(assetId) { 14 | let lr = await LrSession.currentContextP() 15 | let asset = await lr.getAssetP(assetId) 16 | console.log(JSON.stringify(asset, null, 2)) 17 | } 18 | 19 | mainP(process.argv[2]).then(() => console.log('done')).catch(e => console.error('error:', e)) 20 | -------------------------------------------------------------------------------- /samples/src/node/get/getasset2048.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | import FileUtils from '../../common/file/FileUtils.mjs' 13 | 14 | async function mainP(assetId) { 15 | let lr = await LrSession.currentContextP() 16 | let buffer = await lr.getAssetRenditionP(assetId, '2048') 17 | let name = `${assetId}.2048.jpg` 18 | await FileUtils.writeBufferToFileP(buffer, name) 19 | console.log('success: ', name) 20 | } 21 | 22 | mainP(process.argv[2]).then(() => console.log('done')).catch(e => console.error('error:', e)) 23 | -------------------------------------------------------------------------------- /samples/src/node/get/getassetthumb.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | import FileUtils from '../../common/file/FileUtils.mjs' 13 | 14 | async function mainP(assetId) { 15 | let lr = await LrSession.currentContextP() 16 | let buffer = await lr.getAssetRenditionP(assetId, 'thumbnail2x') 17 | let name = `${assetId}.thumb.jpg` 18 | await FileUtils.writeBufferToFileP(buffer, name) 19 | console.log('success: ', name) 20 | } 21 | 22 | mainP(process.argv[2]).then(() => console.log('done')).catch(e => console.error('error:', e)) 23 | -------------------------------------------------------------------------------- /samples/src/node/get/getcollections.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | 13 | async function mainP() { 14 | let lr = await LrSession.currentContextP() 15 | let response = await lr.getAlbumsP('collection_set%3Bcollection') 16 | console.log(JSON.stringify(response, null, 2)) 17 | } 18 | 19 | mainP().then(() => console.log('done')).catch(e => console.error('error:', e)) 20 | -------------------------------------------------------------------------------- /samples/src/node/get/getfirstalbumasset.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | 13 | async function mainP(albumId) { 14 | let lr = await LrSession.currentContextP() 15 | let response = await lr.getFirstAlbumAssetP(albumId) 16 | console.log(JSON.stringify(response, null, 2)) 17 | } 18 | 19 | mainP(process.argv[2]).then(() => console.log('done')).catch(e => console.error('error:', e)) 20 | -------------------------------------------------------------------------------- /samples/src/node/get/getfirstasset.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | 13 | async function mainP() { 14 | let lr = await LrSession.currentContextP() 15 | let response = await lr.getFirstAssetP() 16 | console.log(JSON.stringify(response, null, 2)) 17 | } 18 | 19 | mainP().then(() => console.log('done')).catch(e => console.error('error:', e)) 20 | -------------------------------------------------------------------------------- /samples/src/node/get/getincompletes.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | 13 | async function mainP() { 14 | let lr = await LrSession.currentContextP() 15 | let response = await lr.getIncompletesP() 16 | console.log(JSON.stringify(response, null, 2)) 17 | } 18 | 19 | mainP().then(() => console.log('done')).catch(e => console.error('error:', e)) 20 | -------------------------------------------------------------------------------- /samples/src/node/health/gethealth.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrAuth from '../../common/lr/LrAuth.mjs' 12 | import LrRequestor from '../../common/lr/LrRequestor.mjs' 13 | 14 | async function mainP() { 15 | let response = await LrRequestor.healthP(LrAuth.getApiKey(), LrAuth.getHost()) 16 | console.log(JSON.stringify(response, null, 2)) 17 | } 18 | 19 | mainP().then(() => console.log('done')).catch(e => console.error('error:', e)) 20 | -------------------------------------------------------------------------------- /samples/src/node/upload/uploadfile.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrSession from '../../common/lr/LrSession.mjs' 12 | import File from '../common/file/File.mjs' 13 | import OriginalUtils from '../../common/original/OriginalUtils.mjs' 14 | 15 | async function mainP(filePath) { 16 | let lr = await LrSession.currentContextP() 17 | let orig = await File.originalP(filePath) 18 | let assetId = await OriginalUtils.uploadP(lr, orig) 19 | console.log(`asset id: ${assetId}`) 20 | } 21 | 22 | let filePath = process.argv[2] 23 | mainP(filePath).then(() => console.log('done')).catch(e => console.error(e)) 24 | -------------------------------------------------------------------------------- /samples/src/web/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .cache/ 3 | dist/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /samples/src/web/README.md: -------------------------------------------------------------------------------- 1 | ## Web Samples 2 | 3 | Tested with latest Safari and Chrome and Node.js 14.15.0. 4 | 5 | --- 6 | 7 | This directory contains various examples using the Lightroom Services APIs in a web application. 8 | 9 | ### Prerequisites 10 | 11 | * Developers must create an application integration with the `Lightroom Services` as described in the [Adobe Lightroom developer documentation](https://developer.adobe.com/lightroom). This process will generate an `API Key` and `Client Secret`. 12 | 13 | * Developers must acquire an OAuth 2.0 `user access token` for a Lightroom customer as detailed in the [Adobe Developer authentication documentation](https://developer.adobe.com/authentication/auth-methods.html#!AdobeDocs/adobeio-auth/master/OAuth/OAuth.md), with the `openid`, `lr_partner_apis` and `lr_partner_rendition_apis` scopes. 14 | 15 | * The samples acquire the API Key and user access token through the 16 | `process.env.KEY` and `process.env.TOKEN` environment variables. 17 | These are set by creating a `.env` file in the directory containing 18 | this README: 19 | 20 | KEY='' 21 | TOKEN='' 22 | 23 | * The samples are served through [Parcel](https://parceljs.org), which can be installed with npm, along with any other dependencies: 24 | 25 | npm ci 26 | 27 | ### Common Code 28 | 29 | Common code shared by the web application samples can be found in the `common` directory. It consists of: 30 | 31 | * `common/file`: Modules that leverage the `../common/original/OriginalUtils.mjs` media abstraction to support uploading files to Lightroom from the browser. 32 | 33 | * `common/grid`: A LitElement that displays assets in a user catalog or album in a grid. Supports an on-click event for parent applications to interact with individual assets. 34 | 35 | * `common/image`: Modules that manage loading asset renditions and creating/revoking ObjectURLs so they can be displayed as img.src attributes in the DOM. 36 | 37 | * `common/info`: Basic information display suitable for headers. 38 | 39 | ### Samples 40 | 41 | The `npx` command can be used to have Parcel package and host the given sample: `npx parcel /index.html`. To view the sample, browse to http://localhost:1234/index.html. 42 | 43 | * Asset Picker 44 | 45 | npx parcel pickasset/index.html 46 | 47 | This example demonstrates browsing assets contained in the catalog of an authenticated Adobe Lightroom customer. 48 | 49 | The application header is from `common/info/InfoView.js`. The application first attempts to load the account and catalog information of the Lightroom user associated with the provided user access token. Then the album hierarchy (of subtype "collection") is generated. On success, the user status is displayed in the header. 50 | 51 | The album hierarchy is shown in the left-hand panel. When an album is selected, a grid of the album asset thumbnails is displayed in the well. 52 | 53 | Clicking on any asset in the grid will fetch the 2048 rendition and display it in a scrollable overlay. Clicking the overlay will dispatch the overlay and release the associated rendition. 54 | 55 | * Columnal Asset Browser 56 | 57 | npx parcel browsecolumn/index.html 58 | 59 | This example demonstrates browsing assets contained in the catalog of an authenticated Adobe Lightroom customer. 60 | 61 | The application header is from `common/info/InfoView.js`. The application first attempts to load the account and catalog information of the Lightroom user associated with the provided user access token. Then the album hierarchy (of subtype "collection") is generated. On success, the user status is displayed in the header. 62 | 63 | The album hierarchy is shown in a single column. When an album is selected, a grid of the album asset thumbnails is displayed in the column. 64 | 65 | * Image Uploader 66 | 67 | npx parcel upload/index.html 68 | 69 | The application header is from `common/info/InfoView.js`. The application first attempts to load the account and catalog information of the Lightroom user associated with the provided user access token. On success, the user status is displayed in the header. 70 | 71 | This example uses a file `input` dialog to enable a user to select an asset to upload to the Lightroom catalog. After an asset is successfully loaded, it is appended to an asset grid underneath the header. 72 | 73 | If the file to uploaded is found to be a duplicate of an existing asset, the existing asset is shown in the asset grid. 74 | 75 | * User Info 76 | 77 | npx parcel userinfo/index.html 78 | 79 | The application attempts to load the account and catalog information of the Lightroom user associated with the provided user access token. On success, some account information is displayed in the header populated as a `common/info/InfoView.js`. Otherwise, the reason for the failure is given. 80 | -------------------------------------------------------------------------------- /samples/src/web/aborttest/index.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/src/web/aborttest/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrAuth from '../../common/lr/LrAuth.mjs' 12 | import RequestUtils from '../../common/request/RequestUtils' 13 | 14 | let _abortHealthXHRP = function(apiKey, delay) { 15 | let headers = { 16 | 'X-API-Key': apiKey, 17 | 'Cache-Control': 'no-cache, no-store, must-revalidate' 18 | } 19 | let options = { 20 | method: 'GET', 21 | protocol: 'http:', 22 | host: 'localhost', 23 | port: '8000', 24 | path: '/v2/health', 25 | headers 26 | } 27 | let signal = { 28 | onabort: () => console.log('uncaught abort') 29 | } 30 | let promise = RequestUtils.xhrP(options, undefined, signal) 31 | setTimeout(() => { 32 | signal.onabort() 33 | }, delay) 34 | return promise 35 | } 36 | 37 | let _abortHealthFetchP = function(apiKey, delay) { 38 | let headers = { 39 | 'X-API-Key': apiKey, 40 | 'Cache-Control': 'no-cache, no-store, must-revalidate' 41 | } 42 | let options = { 43 | method: 'GET', 44 | protocol: 'http:', 45 | host: 'localhost:8000', 46 | path: '/v2/health', 47 | headers 48 | } 49 | let controller = new AbortController() 50 | let promise = RequestUtils.fetchP(options, undefined, controller.signal) 51 | setTimeout(() => { 52 | controller.abort() 53 | }, delay) 54 | return promise 55 | } 56 | 57 | async function mainP() { 58 | console.log('start xhr test...') 59 | try { 60 | let health = await _abortHealthXHRP(LrAuth.getApiKey(), 10000) 61 | console.log('expecting health:', health) 62 | } 63 | catch (err) { 64 | console.log('unexpected error', err) 65 | } 66 | 67 | try { 68 | let health = await _abortHealthXHRP(LrAuth.getApiKey(), 1000) 69 | console.log('unexpected health:', health) 70 | } 71 | catch (err) { 72 | console.log('expecting error (aborted)', err) 73 | } 74 | 75 | try { 76 | let health = await _abortHealthXHRP(LrAuth.getApiKey(), 6000) 77 | console.log('expecting health:', health) 78 | } 79 | catch (err) { 80 | console.log('unexpected error', err) 81 | } 82 | console.log('xhr test done.') 83 | 84 | console.log('start fetch test...') 85 | try { 86 | let health = await _abortHealthFetchP(LrAuth.getApiKey(), 10000) 87 | console.log('expecting health:', health) 88 | } 89 | catch (err) { 90 | console.log('unexpected error', err) 91 | } 92 | 93 | try { 94 | let health = await _abortHealthFetchP(LrAuth.getApiKey(), 1000) 95 | console.log('unexpected health:', health) 96 | } 97 | catch (err) { 98 | console.log('expecting error (aborted)', err) 99 | } 100 | 101 | try { 102 | let health = await _abortHealthFetchP(LrAuth.getApiKey(), 6000) 103 | console.log('expecting health:', health) 104 | } 105 | catch (err) { 106 | console.log('unexpected error', err) 107 | } 108 | console.log('fetch test done.') 109 | } 110 | 111 | mainP() 112 | -------------------------------------------------------------------------------- /samples/src/web/browsecolumn/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /samples/src/web/browsecolumn/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import { html, render } from 'lit-html' 12 | import './styles.css' 13 | import InfoView from '../common/info/InfoView' 14 | import LrSession from '../../common/lr/LrSession' 15 | import LrUtils from '../../common/lr/LrUtils' 16 | import LrAssetThumbnailManager from '../common/image/LrAssetThumbnailManager' 17 | import '../common/grid/LrGrid' 18 | 19 | function InsertFolderView(container, folderRoot, onSelectedChanged) { 20 | const _breadcrumbTemplate = (node, onClick) => html` 21 |
onClick(node) }}> 22 | 25 |
26 | ` 27 | 28 | const _nodeIcon = (node) => { 29 | if (!node.data || node.data.type === 'catalog') { 30 | return html `` 31 | } 32 | if (node.data.subtype === 'collection_set') { 33 | return html` 34 | 35 | 36 | 37 | ` 38 | } 39 | return html `` 40 | } 41 | 42 | const _nodeTemplate = (node, onClick) => html` 43 |
onClick(node) }> 44 | 48 |
49 | ` 50 | 51 | const _navTemplate = (node, onClick) => html` 52 | 60 | ` 61 | 62 | const navTemp = (node, onClick) => html` 63 |
64 | ${ _breadcrumbTemplate(node, (node) => onClick(node.parent)) } 65 |
66 |
67 | ${ node.data && (node.data.subtype === 'collection' || node.data.type === 'catalog') 68 | ? html ` 69 | onSelectedChanged(event.detail) }> 71 | 72 | ` 73 | : html ` 74 |
75 | ${ _navTemplate(node, onClick) } 76 |
77 | `} 78 |
79 | ` 80 | 81 | let update = (node) => render(navTemp(node, (node) => { 82 | if (node) { 83 | update(node) 84 | } 85 | }), container) 86 | 87 | update(folderRoot) 88 | } 89 | 90 | async function mainP() { 91 | // construct the top-level page with a header, left hand panel, and well 92 | let header = document.createElement('div') 93 | header.className = 'header' 94 | document.body.appendChild(header) 95 | 96 | let column = document.createElement('div') 97 | column.className = 'column' 98 | document.body.appendChild(column) 99 | 100 | // authenticate; check account and catalog; load album hierarchy; and show status 101 | let infoView = InfoView() 102 | header.appendChild(infoView.element) 103 | let lr 104 | let folderRoot 105 | try { 106 | lr = await LrSession.currentContextP() 107 | infoView.user = `${ lr.account.full_name } (${ lr.account.email })` 108 | infoView.entitlement = lr.account.entitlement.status 109 | 110 | folderRoot = { name: '\u25C3 Back', folders: [], albums: [] } 111 | let root = await LrUtils.getRootCollectionSetP(lr) 112 | folderRoot.folders.push({ 113 | parent: folderRoot, 114 | name: 'All Photos', 115 | folders: [], 116 | albums: [], 117 | data: lr.catalog 118 | }) 119 | root.parent = folderRoot 120 | folderRoot.folders.push(root) 121 | infoView.status = 'album hierarchy loaded' 122 | } 123 | catch (err) { 124 | infoView.status = err.message 125 | return // bail out 126 | } 127 | 128 | let thumbnailManager = new LrAssetThumbnailManager(lr._session, lr.account, lr.catalog) 129 | 130 | // album grid in the well 131 | let albumGridComponentConstructor = window.customElements.get('lr-samples-grid') 132 | albumGridComponentConstructor.session = lr._session 133 | albumGridComponentConstructor.account = lr.account 134 | albumGridComponentConstructor.catalog = lr.catalog 135 | albumGridComponentConstructor.imageManager = thumbnailManager 136 | 137 | let onSelectedChanged = selection => console.log('got selection', selection) 138 | InsertFolderView(column, folderRoot, onSelectedChanged) 139 | } 140 | 141 | mainP() 142 | -------------------------------------------------------------------------------- /samples/src/web/browsecolumn/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | body { 12 | background-color: var(--spectrum-global-color-gray-100); 13 | margin: 0px; 14 | padding: 0px; 15 | overflow: hidden; 16 | font-size: 14px; 17 | font-family: var(--spectrum-font-family-base); 18 | font-weight: 100; 19 | line-height: 20px; 20 | position: absolute; 21 | width: 100%; 22 | height: 100%; 23 | } 24 | 25 | .header { 26 | color: var(--spectrum-global-color-gray-200); 27 | background-color: var(--spectrum-global-color-gray-700); 28 | position: absolute; 29 | left: 0; 30 | right: 0; 31 | top: 0; 32 | height: 84px; 33 | padding: 10px; 34 | box-sizing: border-box; 35 | border-width: 2px; 36 | border-color: var(--spectrum-global-color-gray-50); 37 | border-top-style: solid; 38 | border-bottom-style: solid; 39 | } 40 | 41 | .column { 42 | display: block; 43 | position: absolute; 44 | top: 84px; 45 | bottom: 0; 46 | left: 0; 47 | right: 0; 48 | padding: 10px; 49 | margin-left: 100px; 50 | margin-right: 100px; 51 | background-color: var(--spectrum-global-color-gray-200); 52 | } 53 | 54 | .columnheader { 55 | display: block; 56 | position: absolute; 57 | top: 0px; 58 | height: 40px; 59 | left: 0; 60 | right: 0; 61 | background-color: var(--spectrum-global-color-gray-500); 62 | } 63 | 64 | .columnbody { 65 | display: block; 66 | position: absolute; 67 | top: 40px; 68 | bottom: 0; 69 | left: 0; 70 | right: 0; 71 | } 72 | 73 | .columnscroller { 74 | display: block; 75 | position: absolute; 76 | top: 0; 77 | bottom: 0; 78 | left: 0; 79 | right: 0; 80 | overflow-x: hidden; 81 | overflow-y: auto; 82 | } 83 | 84 | .columnscroller::-webkit-scrollbar { 85 | width: 10px; 86 | height: 10px; 87 | } 88 | 89 | .columnscroller::-webkit-scrollbar-thumb { 90 | background-color: var(--spectrum-global-color-gray-500); 91 | border-radius: 8px; 92 | width: 8px; 93 | height: 8px; 94 | border-top: 2px solid rgba(0, 0, 0, 0); 95 | border-bottom: 2px solid rgba(0, 0, 0, 0); 96 | background-clip: padding-box; 97 | } 98 | 99 | .columnscroller::-webkit-scrollbar-track, .columnscroller::-webkit-scrollbar-track-piece { 100 | background: var(--spectrum-global-color-gray-200); 101 | } 102 | -------------------------------------------------------------------------------- /samples/src/web/common/file/File.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import FileDeferred from './FileDeferred' 12 | import OriginalUtils from '../../../common/original/OriginalUtils' 13 | 14 | let File = { 15 | streamP: async (buffer, chunkSize, chunkHandlerP) => { 16 | if (chunkSize === 0) { // return entire buffer in one chunk 17 | await chunkHandlerP(buffer, 0) 18 | return 19 | } 20 | let count = (buffer.byteLength / chunkSize) - 1 // not last chunk 21 | let offset = 0 22 | for (let i = 0; i < count; i++) { 23 | let data = buffer.slice(offset, offset + chunkSize) 24 | await chunkHandlerP(data, offset) 25 | offset += chunkSize 26 | } 27 | let data = buffer.slice(offset) // last chunk 28 | await chunkHandlerP(data, offset) 29 | }, 30 | 31 | streamController: (deferred) => { 32 | return async (chunkSize, chunkHandlerP) => { 33 | let buffer = await deferred.promise // entire file for sample 34 | await File.streamP(buffer, chunkSize, chunkHandlerP) 35 | } 36 | }, 37 | 38 | original: (file) => { 39 | let deferred = new FileDeferred(file) 40 | return OriginalUtils.create(undefined, undefined, file.name, file.size, File.streamController(deferred)) 41 | } 42 | } 43 | 44 | export default File 45 | -------------------------------------------------------------------------------- /samples/src/web/common/file/FileDeferred.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import FileUtils from './FileUtils' 12 | 13 | class FileDeferred { 14 | constructor(file) { 15 | this._file = file 16 | } 17 | 18 | get promise() { 19 | if (!this._promise) { 20 | this._promise = FileUtils.readP(this._file) 21 | } 22 | return this._promise 23 | } 24 | } 25 | 26 | export default FileDeferred 27 | -------------------------------------------------------------------------------- /samples/src/web/common/file/FileUtils.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | let FileUtils = { 12 | readP: (file) => new Promise((resolve, reject) => { 13 | let reader = new FileReader() 14 | reader.onload = () => resolve(reader.result) 15 | reader.onerror = () => reject(reader.error) 16 | reader.readAsArrayBuffer(file) 17 | }) 18 | } 19 | 20 | export default FileUtils 21 | -------------------------------------------------------------------------------- /samples/src/web/common/grid/LrGrid.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import { LitElement, html, css } from 'lit-element' 12 | import { until } from 'lit-html/directives/until.js' 13 | import LrContext from '../../../common/lr/LrContext' 14 | import LrUtils from '../../../common/lr/LrUtils' 15 | 16 | class LrGrid extends LitElement { 17 | 18 | static _lrContext // current lightroom session 19 | static _session 20 | static _account 21 | static _catalog 22 | static _imageManager 23 | static _cache = {} 24 | 25 | static set session(session) { 26 | this._session = { 27 | accessToken: session.accessToken, 28 | apiKey: session.apiKey, 29 | host: session.host 30 | } 31 | } 32 | 33 | static set account(account) { 34 | this._account = account 35 | } 36 | 37 | static set catalog(catalog) { 38 | this._catalog = catalog 39 | } 40 | 41 | static set imageManager(manager) { 42 | this._imageManager = manager 43 | } 44 | 45 | static get _lr() { 46 | if (!this._lrContext) { 47 | this._lrContext = new LrContext(this._session, this._account, this._catalog) 48 | } 49 | return this._lrContext 50 | } 51 | 52 | static get properties() { 53 | return { 54 | context: { type: String, reflect: true }, 55 | _assets: { type: Array }, 56 | } 57 | } 58 | 59 | constructor() { 60 | super() 61 | this._assets = [] 62 | } 63 | 64 | static get styles() { 65 | return css` 66 | .container { 67 | width: 100%; 68 | height: 100%; 69 | overflow-x: hidden; 70 | overflow-y: auto; 71 | } 72 | .container::-webkit-scrollbar { 73 | width: 10px; 74 | height: 10px; 75 | } 76 | .container::-webkit-scrollbar-thumb { 77 | background-color: var(--spectrum-global-color-gray-500); 78 | border-radius: 8px; 79 | width: 8px; 80 | height: 8px; 81 | border-top: 2px solid rgba(0, 0, 0, 0); 82 | border-bottom: 2px solid rgba(0, 0, 0, 0); 83 | background-clip: padding-box; 84 | } 85 | .container::-webkit-scrollbar-track, .container::-webkit-scrollbar-track-piece { 86 | background: var(--spectrum-global-color-gray-200); 87 | } 88 | .grid { 89 | display: flex; 90 | flex-direction: row; 91 | flex-wrap: wrap; 92 | box-sizing: border-box; 93 | padding: 1px; 94 | width: 100%; 95 | } 96 | img { 97 | margin: 1px; 98 | cursor: pointer; 99 | } 100 | ` 101 | } 102 | 103 | attributeChangedCallback(name, oldValue, newValue) { 104 | if (name == 'context' && newValue) { 105 | const { source, preselected } = JSON.parse(newValue) 106 | 107 | if (!LrGrid._cache) { 108 | console.error('need to initialize samples album grid component') 109 | return 110 | } 111 | 112 | if (source.type === 'album') { 113 | if (!LrGrid._cache[source.id]) { 114 | LrGrid._cache[source.id] = LrGrid._lr.getAlbumAssetsP(source.id) 115 | } 116 | LrGrid._cache[source.id].then((albumAssets) => { 117 | this._assets = albumAssets.map((albumAsset) => albumAsset.asset) 118 | }) 119 | } 120 | if (source.type === 'catalog') { 121 | if (!LrGrid._cache[source.id]) { 122 | LrGrid._cache[source.id] = LrGrid._lr.getFirstPageOfAssetsP() 123 | } 124 | LrGrid._cache[source.id].then((assets) => this._assets = assets) 125 | } 126 | } 127 | } 128 | 129 | _onAssetClick(asset) { 130 | let selection = { 131 | selected: [ asset ], 132 | deselected: [] 133 | } 134 | this.dispatchEvent(new CustomEvent('selected-changed', { detail: selection })) 135 | } 136 | 137 | _getCachedAssetThumbnailObjectURLP(asset) { 138 | if (LrGrid._imageManager) { 139 | return LrGrid._imageManager.getAssetRenditionObjectURLP(asset.id, 'thumbnail2x') 140 | } 141 | return '' 142 | } 143 | 144 | render() { 145 | return html`
146 | ${ this._assets.map((asset) => html` 147 | this._onAssetClick(asset) }> 150 | 151 | `) } 152 |
` 153 | } 154 | } 155 | 156 | customElements.define('lr-samples-grid', LrGrid) 157 | -------------------------------------------------------------------------------- /samples/src/web/common/image/LrAssetRenditionProvider.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | class LrAssetRenditionProvider { 12 | constructor(lr, assetId, type) { 13 | this._disposed = false 14 | let request = lr.getAssetRenditionP(assetId, type) 15 | this.promise = request.then(buffer => this._createObjectURL(buffer)) 16 | } 17 | 18 | _createObjectURL(buffer) { 19 | if (this._disposed) { 20 | return '' 21 | } 22 | let blob = buffer ? new Blob([ new Uint8Array(buffer) ], { type: 'image/jpeg' }) : null 23 | return this._objectURL = blob ? URL.createObjectURL(blob) : '' 24 | } 25 | 26 | dispose() { 27 | // manually manage the objectURL because it is not garbage collected 28 | this._disposed = true 29 | this.promise = Promise.resolve('') 30 | URL.revokeObjectURL(this._objectURL) 31 | } 32 | } 33 | 34 | export default LrAssetRenditionProvider 35 | -------------------------------------------------------------------------------- /samples/src/web/common/image/LrAssetThumbnailManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import LrContext from '../../../common/lr/LrContext' 12 | import LrAssetRenditionProvider from './LrAssetRenditionProvider' 13 | 14 | class LrAssetThumbnailManager { 15 | constructor(session, account, catalog) { 16 | this._lr = new LrContext(session, account, catalog) 17 | this._providerHash = {} 18 | } 19 | 20 | clear() { 21 | Object.values(this._providerHash).forEach(provider => provider.dispose()) 22 | this._providerHash = {} 23 | } 24 | 25 | getAssetRenditionObjectURLP(assetId, type) { 26 | if (!assetId || !this._providerHash) { 27 | return Promise.resolve('') 28 | } 29 | if (!this._providerHash[assetId]) { 30 | this._providerHash[assetId] = new LrAssetRenditionProvider(this._lr, assetId, type) 31 | } 32 | return this._providerHash[assetId].promise 33 | } 34 | } 35 | 36 | export default LrAssetThumbnailManager 37 | -------------------------------------------------------------------------------- /samples/src/web/common/info/InfoView.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | .info { 12 | display: block; 13 | position: relative; 14 | width: 100%; 15 | height: 20px; 16 | margin-left: 8px; 17 | } 18 | -------------------------------------------------------------------------------- /samples/src/web/common/info/InfoView.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import './InfoView.css' 12 | 13 | class InfoView { 14 | constructor() { 15 | this.element = document.createElement('div') 16 | 17 | let user = document.createElement('div') 18 | user.className = 'info' 19 | this._user = document.createTextNode('loading user...') 20 | user.appendChild(this._user) 21 | this.element.appendChild(user) 22 | 23 | let entitlement = document.createElement('div') 24 | entitlement.className = 'info' 25 | this._entitlement = document.createTextNode('loading entitlement...') 26 | entitlement.appendChild(this._entitlement) 27 | this.element.appendChild(entitlement) 28 | 29 | let status = document.createElement('div') 30 | status.className = 'info' 31 | this._status = document.createTextNode('loading status...') 32 | status.appendChild(this._status) 33 | this.element.appendChild(status) 34 | } 35 | 36 | set user(user) { 37 | let node = document.createTextNode(user) 38 | this._user.replaceWith(node) 39 | this._user = node 40 | } 41 | 42 | set entitlement(entitlement) { 43 | let node = document.createTextNode(entitlement) 44 | this._entitlement.replaceWith(node) 45 | this._entitlement = node 46 | } 47 | 48 | set status(status) { 49 | let node = document.createTextNode(status) 50 | this._status.replaceWith(node) 51 | this._status = node 52 | } 53 | } 54 | 55 | export default () => new InfoView() 56 | -------------------------------------------------------------------------------- /samples/src/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lr-partner-apis-samples-web", 3 | "version": "1.0.0", 4 | "description": "Web samples using the Lightroom Services APIs", 5 | "keywords": [], 6 | "author": "", 7 | "license": "MIT", 8 | "browserslist": "last 2 chrome version, last 2 firefox version, last 2 safari version", 9 | "scripts": { 10 | "preinstall": "npx npm-force-resolutions" 11 | }, 12 | "resolutions": { 13 | "@babel/preset-env": "7.13.8" 14 | }, 15 | "devDependencies": { 16 | "parcel-bundler": "^1.12.4" 17 | }, 18 | "dependencies": { 19 | "@spectrum-css/icon": "^3.0.0-beta.2", 20 | "@spectrum-css/sidenav": "^3.0.0-beta.5", 21 | "@spectrum-css/typography": "^3.0.0-beta.1", 22 | "@spectrum-css/vars": "^3.0.0-beta.2", 23 | "lit-element": "^2.4.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/src/web/pickasset/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /samples/src/web/pickasset/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import { html, render } from 'lit-html' 12 | import { until } from 'lit-html/directives/until.js' 13 | import './styles.css' 14 | import InfoView from '../common/info/InfoView' 15 | import LrSession from '../../common/lr/LrSession' 16 | import LrUtils from '../../common/lr/LrUtils' 17 | import LrAssetThumbnailManager from '../common/image/LrAssetThumbnailManager' 18 | import LrAssetRenditionProvider from '../common/image/LrAssetRenditionProvider' 19 | import '../common/grid/LrGrid' 20 | 21 | function InsertFolderView(container, folderRoot, coverThumbnailP, onClickAlbum) { 22 | const _allPhotosTemplate = (onClick) => html` 23 |
24 | 27 |
28 | ` 29 | 30 | const _breadcrumbTemplate = (node, onClick) => html` 31 |
{ if (node.parent) onClick(node) }}> 32 | 35 |
36 | ` 37 | 38 | const _folderTemplate = (node, onClick) => html` 39 |
onClick(node) }> 40 | 46 |
47 | ` 48 | 49 | const _albumTemplate = (coverThumbnailP, album, onClick) => html` 50 |
onClick(album) }> 51 | 55 |
56 | ` 57 | 58 | const navTemplate = (coverThumbnailP, node, onClickFolder, onClickAlbum) => html` 59 | 69 | ` 70 | 71 | let update = (node) => render(navTemplate(coverThumbnailP, node, (node) => update(node), onClickAlbum), container) 72 | update(folderRoot) 73 | } 74 | 75 | async function Insert2048Rendition(container, lr, assetId) { 76 | if (!assetId) { 77 | return 78 | } 79 | 80 | let provider = new LrAssetRenditionProvider(lr, assetId, '2048') 81 | 82 | // create a popover div to display the rendition 83 | let img = document.createElement('img') 84 | let rendition = document.createElement('div') 85 | rendition.className = 'popover' 86 | rendition.appendChild(img) 87 | rendition.onclick = () => { 88 | provider.dispose() 89 | rendition.remove() 90 | } 91 | container.appendChild(rendition) 92 | 93 | // fetch the 2048 image and update the image source with it 94 | try { 95 | await provider.promise.then(objectURL => img.src = objectURL) 96 | } 97 | catch (err) { 98 | console.log('failed to fetch the 2048 rendition of asset:', assetId) 99 | } 100 | } 101 | 102 | async function mainP() { 103 | // construct the top-level page with a header, left hand panel, and well 104 | let header = document.createElement('div') 105 | header.className = 'header' 106 | document.body.appendChild(header) 107 | 108 | let lhp = document.createElement('div') 109 | lhp.className = 'lhp' 110 | document.body.appendChild(lhp) 111 | 112 | let well = document.createElement('div') 113 | well.className = 'well' 114 | document.body.appendChild(well) 115 | 116 | // authenticate; check account and catalog; load album hierarchy; and show status 117 | let infoView = InfoView() 118 | header.appendChild(infoView.element) 119 | let lr 120 | let folderRoot 121 | try { 122 | lr = await LrSession.currentContextP() 123 | infoView.user = `${ lr.account.full_name } (${ lr.account.email })` 124 | infoView.entitlement = lr.account.entitlement.status 125 | 126 | folderRoot = await LrUtils.getRootCollectionSetP(lr) 127 | infoView.status = 'album hierarchy loaded' 128 | } 129 | catch (err) { 130 | infoView.status = err.message 131 | return // bail out 132 | } 133 | 134 | let thumbnailManager = new LrAssetThumbnailManager(lr._session, lr.account, lr.catalog) 135 | 136 | // album grid in the well 137 | let albumGridComponentConstructor = window.customElements.get('lr-samples-grid') 138 | albumGridComponentConstructor.session = lr._session 139 | albumGridComponentConstructor.account = lr.account 140 | albumGridComponentConstructor.catalog = lr.catalog 141 | albumGridComponentConstructor.imageManager = thumbnailManager 142 | let picker = new albumGridComponentConstructor() 143 | picker.addEventListener('selected-changed', (event) => { 144 | let selection = event.detail 145 | let asset = selection && selection.selected[0] // first selected 146 | Insert2048Rendition(document.body, lr, asset.id) 147 | }) 148 | let activeAlbumChanged = (album) => { 149 | let source = album ? album : lr.catalog // if no album, show all photos 150 | let context = JSON.stringify({ 151 | source, 152 | preselected: [] 153 | }) 154 | picker.setAttribute('context', context) 155 | } 156 | well.appendChild(picker) 157 | 158 | // create a folder view in the left hand panel 159 | let coverThumbnailP = async (album) => { 160 | let assetId = await lr.getAlbumCoverOrFallbackAssetIdP(album) 161 | return thumbnailManager.getAssetRenditionObjectURLP(assetId, 'thumbnail2x') 162 | } 163 | InsertFolderView(lhp, folderRoot, coverThumbnailP, activeAlbumChanged) 164 | } 165 | 166 | mainP() 167 | -------------------------------------------------------------------------------- /samples/src/web/pickasset/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | body { 12 | background-color: var(--spectrum-global-color-gray-100); 13 | margin: 0px; 14 | padding: 0px; 15 | overflow: hidden; 16 | font-size: 14px; 17 | font-family: var(--spectrum-font-family-base); 18 | font-weight: 100; 19 | line-height: 20px; 20 | position: absolute; 21 | width: 100%; 22 | height: 100%; 23 | } 24 | 25 | .header { 26 | color: var(--spectrum-global-color-gray-200); 27 | background-color: var(--spectrum-global-color-gray-700); 28 | position: absolute; 29 | left: 0; 30 | right: 0; 31 | top: 0; 32 | height: 84px; 33 | padding: 10px; 34 | box-sizing: border-box; 35 | border-width: 2px; 36 | border-color: var(--spectrum-global-color-gray-50); 37 | border-top-style: solid; 38 | border-bottom-style: solid; 39 | } 40 | 41 | .lhp { 42 | display: block; 43 | position: absolute; 44 | top: 84px; 45 | bottom: 0; 46 | left: 0; 47 | width: 210px; 48 | padding: 10px; 49 | background-color: var(--spectrum-global-color-gray-200); 50 | overflow-x: hidden; 51 | overflow-y: auto; 52 | } 53 | 54 | .lhp::-webkit-scrollbar { 55 | width: 10px; 56 | height: 10px; 57 | } 58 | 59 | .lhp::-webkit-scrollbar-thumb { 60 | background-color: var(--spectrum-global-color-gray-500); 61 | border-radius: 8px; 62 | width: 8px; 63 | height: 8px; 64 | border-top: 2px solid rgba(0, 0, 0, 0); 65 | border-bottom: 2px solid rgba(0, 0, 0, 0); 66 | background-clip: padding-box; 67 | } 68 | 69 | .lhp::-webkit-scrollbar-track, .lhp::-webkit-scrollbar-track-piece { 70 | background: var(--spectrum-global-color-gray-200); 71 | } 72 | 73 | .well { 74 | position: absolute; 75 | top: 84px; 76 | right: 0; 77 | bottom: 0; 78 | left: 230px; 79 | } 80 | 81 | .popover { 82 | position: absolute; 83 | top: 0px; 84 | left: 0px; 85 | width: 100%; 86 | height: 100%; 87 | background-color: rgba(0, 0, 0, 0.5); 88 | overflow: auto; 89 | } 90 | -------------------------------------------------------------------------------- /samples/src/web/upload/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /samples/src/web/upload/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import { html, render } from 'lit-html' 12 | import { until } from 'lit-html/directives/until.js' 13 | import InfoView from '../common/info/InfoView' 14 | import LrSession from '../../common/lr/LrSession' 15 | import LrUtils from '../../common/lr/LrUtils' 16 | import OriginalUtils from '../../common/original/OriginalUtils' 17 | import LrAssetThumbnailManager from '../common/image/LrAssetThumbnailManager' 18 | import File from '../common/file/File' 19 | import './styles.css' 20 | 21 | function _gridView(assets, container, thumbnailManager) { 22 | const assetGrid = (assets) => html` 23 |
24 | ${ assets.map((asset) => html` 25 | console.log('clicked asset') }> 28 | 29 | `) } 30 |
31 | ` 32 | render(assetGrid(assets), container) 33 | } 34 | 35 | async function mainP() { 36 | // construct the top-level page with a header and container 37 | let header = document.createElement('div') 38 | header.className = 'header' 39 | document.body.appendChild(header) 40 | 41 | let picker = document.createElement('div') 42 | picker.className = 'picker' 43 | document.body.appendChild(picker) 44 | 45 | let container = document.createElement('div') 46 | container.className = 'container' 47 | document.body.appendChild(container) 48 | 49 | // authenticate; check account and catalog, and show status 50 | let infoView = InfoView() 51 | header.appendChild(infoView.element) 52 | let lr 53 | try { 54 | lr = await LrSession.currentContextP() 55 | infoView.user = `${ lr.account.full_name } (${ lr.account.email })` 56 | infoView.entitlement = lr.account.entitlement.status 57 | infoView.status = 'account and catalog found' 58 | } 59 | catch (err) { 60 | infoView.status = err.message 61 | return // bail out 62 | } 63 | 64 | let thumbnailManager = new LrAssetThumbnailManager(lr._session, lr.account, lr.catalog) 65 | 66 | let assets = [] // running array of assets from selected files 67 | 68 | let appendAssetP = async (assetId) => { 69 | console.log('adding another asset to grid', assetId) 70 | let thumbnailExists = await lr.assetRenditionExistsP(assetId, 'thumbnail2x') 71 | if (!thumbnailExists) { 72 | await lr.waitForRenditionP(assetId, 'thumbnail2x') 73 | } 74 | let asset = await lr.getAssetP(assetId) 75 | assets.push(asset) 76 | _gridView(assets, container, thumbnailManager) 77 | } 78 | 79 | let filepicker = document.createElement('input') 80 | filepicker.id = 'filepicker' 81 | filepicker.type = 'file' 82 | filepicker.addEventListener('change', () => { 83 | let input = document.getElementById('filepicker') 84 | let file = input.files[0] 85 | console.log('trying to upload file:', file) 86 | let orig = File.original(file) 87 | OriginalUtils.uploadP(lr, orig) 88 | .then(assetId => appendAssetP(assetId)) 89 | .catch(err => console.log('file upload error', err)) 90 | }) 91 | picker.appendChild(filepicker) 92 | } 93 | 94 | mainP() 95 | -------------------------------------------------------------------------------- /samples/src/web/upload/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | body { 12 | background-color: var(--spectrum-global-color-gray-100); 13 | margin: 0px; 14 | padding: 0px; 15 | overflow: hidden; 16 | font-size: 14px; 17 | font-family: var(--spectrum-font-family-base); 18 | font-weight: 100; 19 | line-height: 20px; 20 | position: absolute; 21 | width: 100%; 22 | height: 100%; 23 | } 24 | 25 | .header { 26 | color: var(--spectrum-global-color-gray-200); 27 | background-color: var(--spectrum-global-color-gray-700); 28 | position: absolute; 29 | left: 0; 30 | right: 0; 31 | top: 0; 32 | height: 84px; 33 | padding: 10px; 34 | box-sizing: border-box; 35 | border-width: 2px; 36 | border-color: var(--spectrum-global-color-gray-50); 37 | border-top-style: solid; 38 | border-bottom-style: solid; 39 | } 40 | 41 | .picker { 42 | position: absolute; 43 | top: 84px; 44 | height: 24px; 45 | right: 0; 46 | left: 0; 47 | background-color: var(--spectrum-global-color-gray-700); 48 | box-sizing: border-box; 49 | border-width: 2px; 50 | border-color: var(--spectrum-global-color-gray-50); 51 | border-bottom-style: solid; 52 | } 53 | 54 | input { 55 | position: absolute; 56 | top: 0; 57 | left: 0; 58 | right: 0; 59 | color: var(--spectrum-global-color-gray-200); 60 | margin-left: 16px; 61 | } 62 | 63 | .container { 64 | position: absolute; 65 | top: 114px; 66 | right: 0; 67 | bottom: 0; 68 | left: 0; 69 | overflow-x: hidden; 70 | overflow-y: auto; 71 | } 72 | -------------------------------------------------------------------------------- /samples/src/web/userinfo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /samples/src/web/userinfo/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | import './styles.css' 12 | import InfoView from '../common/info/InfoView' 13 | import LrSession from '../../common/lr/LrSession' 14 | 15 | async function mainP() { 16 | // construct the top-level page with a header 17 | let header = document.createElement('div') 18 | header.className = 'header' 19 | document.body.appendChild(header) 20 | 21 | // authenticate; check account and catalog; and show entitlement 22 | let infoView = InfoView() 23 | header.appendChild(infoView.element) 24 | try { 25 | let lr = await LrSession.currentContextP() 26 | infoView.user = `${ lr.account.full_name } (${ lr.account.email })` 27 | infoView.entitlement = lr.account.entitlement.status 28 | infoView.status = 'account and catalog found' 29 | } 30 | catch (err) { 31 | infoView.status = err.message 32 | return // bail out 33 | } 34 | } 35 | 36 | mainP() 37 | -------------------------------------------------------------------------------- /samples/src/web/userinfo/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. If you have received this file from a source other than Adobe, 8 | then your use, modification, or distribution of it requires the prior 9 | written permission of Adobe. 10 | */ 11 | body { 12 | background-color: var(--spectrum-global-color-gray-100); 13 | margin: 0px; 14 | padding: 0px; 15 | overflow: hidden; 16 | color: var(--spectrum-global-color-gray-700); 17 | font-size: 14px; 18 | font-family: var(--spectrum-font-family-base); 19 | font-weight: 100; 20 | line-height: 20px; 21 | position: absolute; 22 | width: 100%; 23 | height: 100%; 24 | } 25 | 26 | .header { 27 | color: var(--spectrum-global-color-gray-200); 28 | background-color: var(--spectrum-global-color-gray-700); 29 | position: absolute; 30 | left: 0; 31 | right: 0; 32 | top: 0; 33 | height: 84px; 34 | padding: 10px; 35 | box-sizing: border-box; 36 | border-width: 2px; 37 | border-color: var(--spectrum-global-color-gray-50); 38 | border-top-style: solid; 39 | border-bottom-style: solid; 40 | } 41 | -------------------------------------------------------------------------------- /src/pages/api/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Lightroom API Documentation 3 | description: Adobe Lightroom API Documentation 4 | openAPISpec: https://raw.githubusercontent.com/AdobeDocs/lightroom-public-apis/main/static/swagger.json 5 | --- -------------------------------------------------------------------------------- /src/pages/code-sample/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Code Samples 3 | description: Page for Code Samples 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | # Lightroom Services Sample Code 9 | 10 | Sample code demonstrating the use of the Lightroom Services APIs can be found in a [subdirectory](https://github.com/AdobeDocs/lightroom-public-apis/tree/main/samples) of the Lightroom Services [GitHub repository](https://github.com/AdobeDocs/lightroom-public-apis/tree/main). -------------------------------------------------------------------------------- /src/pages/getting-started/authenticate_customers/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Authenticating Customers 3 | description: Page for Authenticating Customers 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | # OAuth Integration 9 | 10 | Registered partner applications are entitled to access Lightroom customer content with a user _access token_ generated through a standard OAuth workflow provided by the Adobe Identity Management System (IMS). 11 | 12 | **Note: _Following best security practices, user access tokens or refresh tokens must be stored in any backend service in a secure format with encryption at rest._** 13 | 14 | The OAuth workflow is described in detail in the [Adobe IMS Authentication and Authorization](https://developer.adobe.com/authentication/auth-methods.html#!AdobeDocs/adobeio-auth/master/OAuth/OAuth.md) documentation. It requires an API key and client secret obtained by [Creating an Integration](../create_integration). 15 | 16 | Partner applications must include the `lr_partner_apis` and `lr_partner_rendition_apis` scope (along with the standard `openid` and `AdobeID` scopes) to access the Lightroom Services. 17 | 18 | To allow renew of refresh token, additional setup is needed on the API key. Should your application require this capability, you must contact Adobe and describe your needs. 19 | 20 | ## Authentication Sample Code 21 | 22 | Adobe I/O provides two [OAuth samples](https://github.com/AdobeDocs/adobeio-auth/blob/master/OAuth/samples/samples.md) on GitHub for the authentication workflow, one in Node.js and one in Python. 23 | 24 | - To run the [Node.js sample](https://github.com/AdobeDocs/adobeio-auth/blob/master/OAuth/samples/adobe-auth-node), replace the `creative_sdk` scope in _server/index.js_ with `lr_partner_apis,lr_partner_rendition_apis` and follow the instructions in the ReadMe to run the application. 25 | 26 | - To run the [Python example](https://github.com/AdobeDocs/adobeio-auth/blob/master/OAuth/samples/adobe-auth-python), replace the `creative_sdk` scope in _adobe-oauth2.0.py_ with `lr_partner_apis,lr_partner_rendition_apis` and follow the instructions in the ReadMe to run the application. 27 | 28 | Authorization through the Adobe Identify Management System is working as expected if a user is able to successfully log in and view their profile information. 29 | 30 | ## Authorization Workflow Diagrams 31 | 32 | ![OAUTH flow diagram for Lightroom Partner Integration](../../../../static/OAuthFlowDiagram.png) 33 | 34 | ![IMS Token Usage](../../../../static/IMS_Authorization_flow.png) 35 | -------------------------------------------------------------------------------- /src/pages/getting-started/branding_guidelines/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Branding Guidelines 3 | description: Page for Branding Guidelines 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | # Branding Guidelines 9 | 10 | - Adhering to all trademark guidelines is required as noted here: https://www.adobe.com/legal/permissions/trademarks.html 11 | 12 | - For Lightroom, the first and most prominent reference should utilize the full product name – "Adobe Photoshop Lightroom" or "Adobe Photoshop Lightroom Classic", depending on the product – after which just "Lightroom" or "Lightroom Classic" may be used 13 | 14 | - When referring to photos in Lightroom cloud storage, use the Lightroom branding and do not use the Creative Cloud branding -------------------------------------------------------------------------------- /src/pages/getting-started/create_integration/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Creating an Integration 3 | description: Page for Creating an Integration 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | # Creating an Integration 9 | 10 | The process for creating a new partner integration is described in detail in the [Adobe I/O OAuth Integration](https://developer.adobe.com/authentication/auth-methods.html#!AdobeDocs/adobeio-auth/master/AuthenticationOverview/OAuthIntegration.md) documentation. A brief walkthrough for Lightroom Services is: 11 | 12 | 1. Identify an existing Adobe ID with an affiliated email address, or create a new one, to manage the integration. This Adobe ID will have administrative privileges for the lifecycle of the integration. 13 | 14 | 2. Log into the Adobe I/O Console with the chosen Adobe ID. Select "Create new project", and "Add API". 15 | 16 | 3. Under the "Creative Cloud" offerings, select "Lightroom Services" and "Continue". Complete the form as directed in the [Adobe I/O OAuth Integration](https://developer.adobe.com/authentication/auth-methods.html#!AdobeDocs/adobeio-auth/master/AuthenticationOverview/OAuthIntegration.md) documentation to obtain an API key and client secret. 17 | 18 | 4. Verify access to the Lightroom Services through a [Services Health Check](../service_health_check) and by [Authenticating Customers](../authenticate_customers). 19 | -------------------------------------------------------------------------------- /src/pages/getting-started/developer_guidelines/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Developer Guidelines 3 | description: Page for Developer Guidelines 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | # Developer Guidelines 9 | 10 | ## Be respectful of system resource utilization 11 | 12 | - Access resources only once per session and make use of short-lived local cache 13 | 14 | - Only retrieve resources with a high likelihood of immediate user access 15 | 16 | - Limit look ahead to a reasonable number of pages 17 | 18 | - Constrain the size of resources used in fast scrolls to thumbnails 19 | 20 | - Access smaller resources rather than doing local downsizing 21 | 22 | - Limit polling to resources immediately required only 23 | 24 | - Employ a managed local cache to avoid retrieving the same rendition multiple times, but be aware that renditions can change 25 | 26 | - Avoid retrieving and caching unneeded resources 27 | 28 | ## Store only supported file types and valid files in Lightroom 29 | 30 | - Failure to create thumbnails or import a file through a Lightroom application are both indications a given file is not supported 31 | 32 | - Invalid or unsupported files may be deleted 33 | 34 | ## Limit retries 35 | 36 | - Responses with a 500 may indicate a temporary error condition and should be retried, after backing off. 37 | 38 | - Retries should be limited - 3 39 | 40 | - See https://status.adobe.com/ for system status 41 | 42 | - Contact Adobe should 500 errors persist 43 | 44 | - Other http errors should only be retried as guided in the documentation 45 | 46 | ## Use the documentation 47 | 48 | - Undocumented features and functionality may cease to work without notice in a future release. 49 | 50 | - Please discuss any additional API or data needs with Adobe 51 | 52 | Adobe reserves the right to avoid abuse of system resources. Failure to follow this guidance may result in a warning and possible revocation of your license keys -------------------------------------------------------------------------------- /src/pages/getting-started/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Start building with the Lightroom APIs 3 | description: Page for Getting Started 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | # Start building with the Lightroom APIs 9 | 10 | Adobe Photoshop Lightroom stores user _assets_, with their associated metadata and media renditions, in a _catalog_ in the cloud. 11 | 12 | ## Accessing Lightroom Content 13 | 14 | Lightroom content of a Creative Cloud customer is managed through a set of RESTful APIs. These APIs are available only to entitled partner applications that have authenticated the customer, and the customer has given their express permission to the client to act on their behalf. The [API Reference](../api/) documents the available APIs. 15 | 16 | Partners must register a new _integration_ with Adobe to obtain a unique client identifier (_API key_) for their application. Partner applications authenticate Lightroom customers through the Adobe Identity Management System (IMS) using a standard OAuth 2.0 workflow. This process enables a client to obtain an _access token_ that must be included along with the integration API Key in privileged requests to the Lightroom APIs. 17 | -------------------------------------------------------------------------------- /src/pages/getting-started/manage_content/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Manage Content 3 | description: Page for Managing Content 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | # Manage Content 9 | 10 | There are two workflows with which a partner application can manage content: 11 | * Affiliating Content 12 | * Uploading to Lightroom and Managing Content 13 | 14 | ## Affiliating Content 15 | 16 | A partner application can affiliate content in Lightroom with content in its own asset management system, and publish content from Lightroom. 17 | 18 | ## Uploading to Lightroom and Managing Content 19 | 20 | _Note: When a [new asset is created and uploaded](../upload_content/) to a Lightroom catalog, partner applications should always set the `importedOnDevice` field to their API key. This will ensure that the asset is properly tagged in Lightroom as having originated from the partner application. They should also retain the unique identifier of the asset (`asset_id`) and catalog (`catalog_id`) for use in the workflows detailed below._ 21 | 22 | Partner applications that upload new assets to the catalog of a Lightroom customer may want a way to identify those assets, both inside the Lightroom clients as well as in applications on their own services. 23 | 24 | Assets can be grouped together in a catalog through a special album of subtype _project_. Partner applications can create one or more project albums and add uploaded assets to them. Lightroom clients will display project albums of recognized partner applications in their _Connections_ panel, enabling Lightroom customers to further manage the content directly in Lightroom. 25 | 26 | ### Creating a Project Album 27 | 28 | As with the `asset_id` of new assets, partner applications should generate a globally unique identifier for a new project album (`album_id`), conforming to RFC-4122 without hypens. They can then create a new project album with this and the `catalog_id`: 29 | 30 | ``` 31 | PUT /v2/catalogs/{catalog_id}/albums/{album_id} 32 | ``` 33 | 34 | With a body of the form: 35 | 36 | ``` 37 | { 38 | "subtype": "project", 39 | "serviceId": "", 40 | "payload": { 41 | "userCreated": "2012-01-03T04:54:15Z", 42 | "userUpdated": "2012-01-03T04:54:15Z", 43 | "name": "Crivitz", 44 | "publishInfo": { 45 | "version": 3, 46 | "created": "2017-08-03T04:54:32.884643Z", 47 | "updated": "2017-08-03T04:54:32.884643Z", 48 | "deleted": true, 49 | "remoteId": "seRviC3-sp3c1fic", 50 | "remoteLinks": { 51 | "edit": { 52 | "href": "https://external.site.com/editor/albums/afd05f03" 53 | }, 54 | "view": { 55 | "href": "https://external.site.com/albums/afd05f03" 56 | } 57 | }, 58 | "servicePayload": "service-specific string" 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | See the [generic data model page](../guides/common_data_model/) for descriptions of common fields. At the top level of the object is a `serviceId` that must be set to the API key of the partner application. The `payload.name` field holds a user-visible string that will be shown in Lightroom clients when they present the project album. 65 | 66 | The `publishInfo` clause is the place for partner applications to persist information in the Lightroom catalog regarding external content that is affiliated with the project album. Its fields are: 67 | 68 | - `version` (required, integer) Should be 3 for new projects. 69 | - `created` (optional, ISO 8601 date) Date when any externally affiliated content on the partner service was initially created. 70 | - `updated` (optional, ISO 8601 date) Date when any externally affiliated content on the partner service was updated or deleted. Must match the created field when that field is first set. 71 | - `deleted` (optional, boolean) Whether the externally affiliated content has been deleted from the partner service, thereby acting as a tombstone. 72 | - `remoteId` (optional, string) Identifier for the externally affiliated content that is unique to the partner service. 73 | - `remoteLinks` (optional, table): Links to affiliated URLs on the partner service. 74 | - `edit` (optional, table): Table whose "href" entry is an absolute URL to edit the externally affiliated content on the partner service 75 | - `view` (optional, table): Table whose "href" entry is an absolute URL to view the externally affiliated content on the partner service 76 | - `servicePayload` (optional, string) Metadata that is unique to the partner service, encapsulated as a single string with a maximum length of 1024 characters. 77 | 78 | ### Enumerating Project Albums 79 | 80 | Partner applications can enumerate project albums with: 81 | 82 | ``` 83 | GET /v2/catalogs/{catalog_id}/albums?subtype=project 84 | ``` 85 | 86 | It will return an array of project albums in `resources`. If there are no project albums affiliated with the partner application, then the array will be empty. 87 | 88 | ``` 89 | { 90 | "base": "https://lr.adobe.io/v2/catalogs/9479135e/", 91 | "resources": [ 92 | { 93 | "id": "a3c679e3", 94 | "created": "2012-01-03T04:54:32.884643Z", 95 | "updated": "2012-01-03T04:54:32.884643Z", 96 | "type": "album", 97 | "subtype": "project", 98 | "serviceId": "", 99 | "payload": { 100 | "userCreated": "2012-01-03T04:54:15Z", 101 | "userUpdated": "2012-01-03T04:54:15Z", 102 | "name": "Crivitz", 103 | "publishInfo": { 104 | "version": 3, 105 | "created": "2017-08-03T04:54:32.884643Z", 106 | "updated": "2017-08-03T04:54:32.884643Z", 107 | "remoteId": "seRviC3-sp3c1fic", 108 | "remoteLinks": { 109 | "edit": { 110 | "href": "https://external.site.com/editor/albums/afd05f03" 111 | }, 112 | "view": { 113 | "href": "https://external.site.com/albums/afd05f03" 114 | } 115 | }, 116 | "servicePayload": "service-specific string" 117 | } 118 | }, 119 | "links": { 120 | .... 121 | } 122 | }, 123 | ... 124 | ] 125 | } 126 | ``` 127 | 128 | ### Adding Assets to a Project Album 129 | 130 | To add one or more assets to a project album with an ID `album_id` in a catalog with an ID `catalog_id` (up to 50 assets per call), use the API: 131 | 132 | ``` 133 | PUT /v2/catalogs/{catalog_id}/albums/{album_id}/assets 134 | ``` 135 | 136 | with a body of the form: 137 | 138 | ``` 139 | { 140 | "resources": [ 141 | { 142 | "id": "", 143 | "payload": { 144 | "cover": true, 145 | "order": "string", 146 | "publishInfo": { 147 | "remoteId": "seRviC3-sp3c1fic", 148 | "servicePayload": "service-specific string" 149 | } 150 | } 151 | }, 152 | ... 153 | ] 154 | } 155 | ``` 156 | 157 | This will generate a new _album asset_ with the given metadata for each asset in the list. A single asset may be placed in multiple albums in a Lightroom catalog, and the album asset metadata provides a mechanism to attach information to the asset that is unique to each album in which it appears. 158 | 159 | The `publishInfo` clause is the place for partner applications to persist information in the Lightroom catalog regarding external content that is affiliated with each asset being added to the project album. Its fields are: 160 | 161 | - `remoteId` (optional, string) Identifier for the externally affiliated content that is unique to the partner service. 162 | - `servicePayload` (optional, string) Metadata that is unique to the partner service, encapsulated as a single string with a maximum length of 1024 characters. 163 | 164 | Setting the optional `cover` to be `true` will inform Lightroom clients and partner applications that any visual representation of the project album should use that asset as its thumbnail. Only one asset can be the cover. If the project album does not have a cover asset, clients are expected to use the first asset in the album as the cover. 165 | 166 | #### Custom Order 167 | 168 | The optional `order` field supports custom ordering of assets in the project album. The order string shall contain a maximum of 1024 characters in the set: [-0-9A-Z_a-z]. This is the same character set as `base64url` described in RFC 4648 and was chosen for its URL-safeness. It differs from base64url due to the lexicographical sort order: (“-”, “0”, “9”, “A”, “Z”, “_”, “a”, “z”). 169 | 170 | This charset and sort order shall be called _lex64_. The following should be taken into consideration: 171 | 172 | - The absence of an order field will cause that object to be sorted at the end for an ascending sort, or at the beginning for a descending sort. 173 | - The secondary sort field will be the captureDate date of the object (for identical or absent values). 174 | - The tertiary sort field will be the created date of the object (for identical or absent captureDate values). 175 | - The empty string is not a valid order field value. 176 | - In order to preserve the ability to insert at the beginning of the list, the order string cannot end with the “-” character. 177 | 178 | ### Connect Workflow diagrams 179 | ![Connect Albums Workflow](../../../../static/ConnectWorkflowDiagrams.png) -------------------------------------------------------------------------------- /src/pages/getting-started/read_generate_renditions/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Read and Generate Renditions 3 | description: Page for Reading and Generating Renditions 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | # Read and Generate Renditions 9 | 10 | ## Renditions 11 | Each photo asset has a set of JPEGs intended for application display of the latest visual representation of the asset (either edited or original uploaded state) 12 | 13 | ## Details of Renditions 14 | 15 | * Creation - Upon uploading a photo, the following renditions will be created (JPEG format) 16 | * thumbnail2x (320 pixels constraint on either edge) 17 | * 640 (640 pixels on long edge) 18 | * 1280 (1280 pixels on long edge) 19 | * 2048 (2048 pixels on long edge) 20 | 21 | * Edit - Upon editing a photo, the following renditions will be newly created with all edits applied 22 | * thumbnail2x (320 pixels constraint on either edge) 23 | * 640 (640 pixels on long edge) 24 | * 1280 (1280 pixels on long edge) 25 | * 2048 (2048 pixels on long edge) 26 | 27 | For best performance, use the smallest size possible for all application scrolling case 28 | 29 | 30 | ## Workflow to generate renditions for print workflow 31 | 32 | Generate renditions for an original file asynchronously. Allowed rendition types are fullsize and 2560. Generated rendition will be deleted after 1 day automatically. Both these renditions must always be requested on demand. First check to see if one is available, and if not create it. These renditions are not available with basic API access. Should your application require this capability, you must contact Adobe and describe your needs. Here are the details of fullsize and 2560 33 | - Fullsize - size of original with edits, constrained by any applied crops. 34 | - 2560 - 2560 pixels on long edge 35 | 36 | 37 | _STEP 1_: Generate Renditions for an asset asynchronously. 38 | 39 | ``` 40 | POST /v2/catalogs/{catalog_id}/assets/{asset_id}/renditions HTTP/1.1 41 | Authorization: {auth_token} 42 | X-Generate-Renditions: {fullsize,2560} 43 | ``` 44 | 45 | Sample success response: 46 | 47 | ``` 48 | HTTP/1.1 202 49 | ``` 50 | 51 | _STEP 2_: HEAD call for Rendition API. As creation is asynchronous, please poll with exponential back with a timeout of 10 min until the rendition is available. If renditions are not genearted within 10 min then probably the operations has failed. You need to retry from Step 1 in that case. Contact us if the problem persists. 52 | 53 | ``` 54 | HEAD /v2/catalogs/{catalog_id}/assets/{asset_id}/renditions/ HTTP/1.1 55 | Authorization: {auth_token} 56 | ``` 57 | 58 | 59 | Sample success response: 60 | 61 | ``` 62 | HTTP/1.1 200 63 | ``` 64 | 65 | _STEP 3_: If the HEAD call returns 200 successfully then that means rendition has been generated correctly. Now call the read rendition api to get the rendition. 66 | 67 | ``` 68 | GET /v2/catalogs/{catalog_id}/assets/{asset_id}/renditions/ HTTP/1.1 69 | Authorization: {auth_token} 70 | ``` 71 | 72 | Sample success response: 73 | 74 | ``` 75 | HTTP/1.1 200 76 | ``` 77 | 78 | It will return the bits of the rendition 79 | 80 | NOTE: Refer to the API documentation for further information about above listed APIs. 81 | 82 | ### Generate Renditions diagrams 83 | ![Generate Renditions for Lightroom Assets](../../../../static/PrintWorkflow.png) -------------------------------------------------------------------------------- /src/pages/getting-started/service_health_check/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Services Health Check 3 | description: Page for Services Health Check 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | # Lightroom Services Health Check 9 | 10 | Registered partner applications can check the health of the Lightroom Services through the `/v2/health` API on the `https://lr.adobe.io` endpoint. The health check requires only the API key obtained by [Creating an Integration](../create_integration). 11 | 12 | Sample cURL might be: 13 | 14 | ``` 15 | curl -H "X-API-Key: NEW_API_KEY" https://lr.adobe.io/v2/health 16 | ``` 17 | 18 | with an expected response of the form: 19 | 20 | ``` 21 | while (1) {} 22 | {"version":"aaf68f5ea64545693f3add0c309d420d42653bb0"} 23 | ``` 24 | 25 | If the API key is not subscribed to the Lightroom Services, an error will be returned: 26 | 27 | ``` 28 | while (1) {}{"code":"403003", "description":"Api Key is invalid"} 29 | ``` 30 | 31 | Any other error indicates that the Lightroom Services are down. Partner applications are advised to retry the check with an exponential backoff in time until the service is restored. 32 | -------------------------------------------------------------------------------- /src/pages/guides/calling_api/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Calling a Lightroom API 3 | description: Page for Calling a Lightroom API 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | # Calling a Lightroom API 9 | 10 | As described elsewhere, applications must acquire an _API key_ by registering an _integration_ as an Adobe partner. Using the API key to authenticate a Lightroom customer with the Adobe Identity Management System enables the application to acquire an _access token_. 11 | 12 | The API key must be included in the `X-API-Key` header in every API call, while the access token must be included in the `Authorization: Bearer` header. 13 | 14 | Sample cURL for calling an API might be: 15 | 16 | ``` 17 | key=ClientAPIKey 18 | token=eyJ4NXUi... 19 | curl -H "X-API-Key: ${key}" -H "Authorization: Bearer ${token}" https://lr.adobe.io/v2/... 20 | ``` 21 | 22 | and sample JavaScript might be: 23 | 24 | ``` 25 | var key='ClientAPIKey' 26 | var token='eyJ4NXUi...' 27 | var xhr = new XMLHttpRequest() 28 | xhr.open('GET', apiURL) 29 | xhr.setRequestHeader('X-API-Key', key) 30 | xhr.setRequestHeader('Authorization', `Bearer ${token}`) 31 | ``` 32 | 33 | ### Content Type 34 | 35 | Lightroom content is encapsulated in a variety of objects, including _accounts_, _catalogs_, _assets_, and _albums_. The content type of every API is _JSON_, with the exception of the APIs that handle binary data (content type _application/octet-stream_) and those that handle external XMP Develop Settings (content type _application/rdf+xml_). 36 | 37 | ### Handling JSON 38 | 39 | Returned JSON content is always prepended with a `while(1){}` clause to mitigate abuse. This clause must be stripped by client applications before using the incoming result. 40 | 41 | Sample JavaScript for eliding the preface and constructing an object might be: 42 | 43 | ``` 44 | function _processJSONResponse(response) { 45 | let while1Regex = /^while\s*\(\s*1\s*\)\s*{\s*}\s*/ 46 | return response ? JSON.parse(response.replace(while1Regex, '')) : null 47 | } 48 | ``` -------------------------------------------------------------------------------- /src/pages/guides/common_data_model/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Common Data Model 3 | description: Page for Common Data Model 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | # Common JSON Data Model 9 | 10 | The data models for all JSON content share many common elements. Long strings, unique identifiers, and URLs have been truncated here for legibility. 11 | 12 | ``` 13 | { 14 | "base": "https://lr.adobe.io/v2/catalogs/9479135e/", 15 | "id": "a3c679e3", 16 | "created": "2012-01-03T04:54:32.884643Z", 17 | "updated": "2012-01-03T04:54:32.884643Z", 18 | "type": "custom for each type", 19 | "subtype": "custom for each type", 20 | "payload": { 21 | "userCreated": "2012-01-03T04:54:15Z", 22 | "userUpdated": "2012-01-03T04:54:15Z", 23 | // custom for each type 24 | }, 25 | "links": { 26 | "self": { 27 | "href": "custom/for/each/type/a3c679e3" 28 | } 29 | // additional links custom for each type 30 | } 31 | } 32 | ``` 33 | 34 | Unless otherwise noted, all entries at the top level are constructed and provided by the server and can not be modified by clients. 35 | 36 | * `base` (URL): Base URL for any relative HREF values in the data. When items are returned in a list, base will typically not be included with each entry. 37 | * `id` (Lightroom-style UUID): Unique Lightroom identifier of this asset. It is usually generated by client on creation but may be generated by the server in some circumstances. 38 | * `created` (ISO 8601 date): Date when asset was first uploaded to the server. 39 | * `updated` (ISO 8601 date): Date when asset was most recently revised on the server. 40 | * `type` (string): Always the same value for a type. 41 | * `subtype` (string): A different set of legal values is defined for each type. Provided by the client on creation. A subtype is always required for types that support it. 42 | * `payload` (optional, table): Metadata about the object provided by the client. 43 | * Values supported within the payload generally differ for each document type. 44 | * If the payload is completely empty, which is allowed for some types, then it will be omitted. 45 | * `userCreated` (ISO 8601 date): Date and time when the user created this document on the client side. 46 | * `userUpdated` (ISO 8601 date): Date and time when the user last updated this document's payload (or other user modifiable data). This should be set at initial creation and updated by a client each time a user makes a change to data within the payload hash. The userUpdated time should not be updated by automated processes (e.g. reverse geocode, etc.). 47 | * `links` (table): Links to related data. 48 | * Each entry is a table containing an `href` entry with the actual link. 49 | * All types include the `self` link. 50 | * Each type may have other links supported. -------------------------------------------------------------------------------- /src/pages/guides/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using the Lightroom APIs 3 | description: This is the guides overview page of Lightroom 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | # Get Started 9 | 10 | This is the guide to get started with Lightroom APIs. -------------------------------------------------------------------------------- /src/pages/guides/links_and_pagination/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Links and Pagination 3 | description: Page for Links and Pagination 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | # Links and Pagination 9 | 10 | ### Links 11 | 12 | Content returned from the Lightroom APIs often includes a `base` URL and table of relative `links` that should be used when assembling subsequent requests. To ensure the most robust future-proofing, clients should avoid cobbling together their own fully formed URLs when possible. 13 | 14 | For example, the API to retrieve a catalog might return (unique identifiers have been shortened to preserve readability): 15 | 16 | ``` 17 | { 18 | "base": "https://lr.adobe.io/v2/catalogs/18c23e15/", 19 | ... 20 | "links" { 21 | "self": { 22 | "href": "/v2/catalogs/18c23e15" 23 | }, 24 | "/rels/albums": { 25 | "href": "albums" 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | To construct the API endpoint to enumerate the albums in the catalog, an application should concatenate the `base` with the `/rels/albums` value in `href`. When composing these two elements into a single URL, clients should use the standard rules for URI resolution as defined in section 5.2 of RFC 3986. We strongly recommend that clients use standard library implementations to avoid worrying about corner cases of the RFC URI rules. 32 | 33 | ### Pagination 34 | 35 | Indefinitely long lists of content, such as albums and assets, may be paginated during their return. Therefore, a client may need to make multiple calls to enumerate the entire list. The `links` subsection of returned content will contain a `next` field that encapsulates the API call for fetching an additional page of assets. Sample recursive JavaScript might be: 36 | 37 | ``` 38 | function getPagedJSONContentP(url) { 39 | return new Promise(function(resolve, reject) { 40 | var response = { 41 | resources: [] 42 | } 43 | function getPage(url) { 44 | getJSONContentP(url).then(function(page) { 45 | response.base = page.base 46 | response.resources = response.resources.concat(page.resources) 47 | if (page.links.next) { 48 | getPage(page.base + page.links.next.href) 49 | } 50 | else { 51 | resolve(response) 52 | } 53 | }, reject) 54 | } 55 | getPage(url) 56 | }) 57 | } 58 | ``` -------------------------------------------------------------------------------- /src/pages/guides/primitive_data_types/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Primitive Data Types 3 | description: Page for Primitive Data Types 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | # Primitive Data Types 9 | 10 | This page describes data types whose definitions may not be commonly recognized. Unless otherwise stated, these are expressed as strings in the JSON content traversing the Lightroom APIs. 11 | 12 | ### Lowercase UUID 13 | 14 | A universally-unique identifier where the hex digits are represented using lower case letters and the hyphens are omitted. Used to identify virtually all objects in the Lightroom cloud. 15 | 16 | ### User-visible string 17 | 18 | A *user-visible string* is interpreted to mean any string that is valid in JSON, with the following exceptions: 19 | 20 | * UTF8 encoding is expected. 21 | * Unicode control characters (including CR, LF, tab) are disallowed. 22 | * Clients are expected to display these as user-visible strings without further interpretation. Clients are responsible for escaping strings to prevent unwanted interpretations. 23 | * This is specifically _not_ formatted HTML. Any HTML markup that might be entered into the string should _not_ cause the corresponding HTML formatting to occur. Clients are responsible for escaping characters such as <, >, and & to prevent browser side effects. 24 | * User-visible strings must be non-empty. 25 | * Some of these restrictions might be overridden in specific cases. 26 | 27 | 28 | ### ISO 8601 date stamps 29 | 30 | An ISO 8601 date stamp, unless otherwise specified, is a timestamp in the [W3C-specific subset of the ISO 8601 format](http://www.w3.org/TR/NOTE-datetime). 31 | 32 | * Timestamps intended for sorting should be specified in UTC and include the trailing "Z" indicator of such. Clients should use the closest available approximation of UTC. The Lightroom server will always use a highly-accurate UTC time. 33 | * Timestamps intended for user display and grouping (especially date-based grouping) should use local timestamps and include an appropriate designator of the actual time zone. 34 | 35 | #### Cleansing Invalid Datetime Values 36 | 37 | The datetime values provided by clients in some model payloads have validation rules. A datetime might come from metadata generated by a camera or a third party application which may be using nonstandard formatting that does not pass the validation rules. It is recommended that clients check payload values for validity before sending the data to the cloud. Specifically for datetime values, if the value does not pass validation it is recommended that clients attempt some simple fixes to the datetime to retain as much information as possible and to get to a valid value. 38 | 39 | Validating dates locally before sending to the cloud is preferred. If the cloud rejects a date as invalid, it is recommended to remove the date or come up with a suitable replacement for required dates as detailed below, in order to avoid bugs involving confusion and disagreement between the cloud and client on the correct values, which sometimes triggers looping behavior where the client attempts to send the same data to the cloud repeatedly, in some cases thousands of times. 40 | 41 | * If the seconds value is 60, indicating a possible leap second, convert it to 59 42 | * e.g. 2017-03-09T15:47:60.000Z becomes 2017-03-09T15:47:59.000Z 43 | * Try removing the time and submitting the date only. 44 | * Most datetime values in the data model require time, so a client would need to replace the time with 00:00:00 (e.g. 2017-03-09T00:00:00) 45 | * Many times do not require timezone, but some times require a valid timezone and some specifically require GMT timezone with a Z timezone specification (e.g. 2017-03-09T00:00:00Z). For all times requiring a time zone, the recommendation is to specify GMT (Z) timezone. 46 | * Most datetimes in the xmp section of the asset payload allow date only (e.g. 2017-03-09) 47 | * If the date is invalid, then a client will need to omit the field or come up with a suitable replacement for required fields. 48 | * The required field captureDate in the asset payload allows the special value 0000-00-00T00:00:00 for assets with unknown capture date. 49 | * The only other strictly required fields for assets is importSource.importTimestamp, and current time can be substituted for that value. 50 | 51 | ### Reasonable file/folder path name 52 | 53 | A reasonable file or folder path is a UTF-8 string designed to comply with the typical file naming rules on MacOS and Windows without unexpected side effects. -------------------------------------------------------------------------------- /src/pages/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdobeDocs/lightroom-public-apis/e3ab0d7a73c1fdfa812314b59fbdcec3625df639/src/pages/hero.png -------------------------------------------------------------------------------- /src/pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview - Adobe Lightroom API 3 | description: This is the overview page of Adobe Lightroom API 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | 9 | 10 | # Adobe Lightroom API 11 | 12 | Build applications that work with Lightroom 13 | 14 | 15 | 16 | #### Resources 17 | 18 | * [API Documentation](api/) 19 | * [Adobe Lightroom Github Repo](https://github.com/AdobeDocs/lightroom-public-apis) 20 | 21 | ## Welcome to Lightroom API! 22 | 23 | Adobe Photoshop Lightroom stores user assets, with their associated metadata and media renditions, in a catalog in the cloud. 24 | 25 | The [Getting Started](getting-started/) page will walk you through how to use our API. 26 | 27 | ## Contributing 28 | 29 | We encourage you to participate in our open documentation initiative, if you have suggestions, corrections, additions 30 | or deletions for this documentation, check out the source from [this github repo](https://github.com/AdobeDocs/lightroom-public-apis), and submit a pull 31 | request with your contribution. 32 | -------------------------------------------------------------------------------- /src/pages/release-notes/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API Change Logs 3 | description: Page for API Change Logs 4 | contributors: 5 | - https://github.com/bagra98 6 | --- 7 | 8 | # API Change Logs 9 | This document provides information about new features and bug fixes with the Lightroom APIs. 10 | 11 | ## Version 1.2.1 (18 May, 2021) 12 | - Revision IDs have been removed from the response of below APIs. 13 | 1. Get a Catalog Asset (GET) - `/v2/catalogs/{catalog_id}/assets/{asset_id}` 14 | 2. Retrieve Assets (GET) - `/v2/catalogs/{catalog_id}/assets` 15 | 3. List assets of an album (GET)- `/v2/catalogs/{catalog_id}/albums/{album_id}/assets` 16 | 4. Read a album (GET) - `/v2/catalogs/{catalog_id}/albums/{album_id}` 17 | 5. Retrieve albums (GET) - `/v2/catalogs/{catalog_id}/albums` 18 | 19 | 20 | - Added Create Master Links and Create xmp Develop Links to response of below APIs 21 | 1. Get a Catalog Asset (GET) - `/v2/catalogs/{catalog_id}/assets/{asset_id}` 22 | 2. Retrieve Assets (GET) - `/v2/catalogs/{catalog_id}/assets` 23 | 3. List assets of an album (GET)- `/v2/catalogs/{catalog_id}/albums/{album_id}/assets` 24 | 25 | ## Version 1.2.0 (24 Feb, 2021) 26 | - Added new Create Master API (PUT)- `/v2/catalogs/{catalog_id}/assets/{asset_id}/master` . It can be used to upload master for a new asset. 27 | 28 | - Existing Create Master API (PUT) - `/v2/catalogs/{catalog_id}/assets/{asset_id}/revisions/{revision_id}/master` is removed and will be deprecated soon. Please use above mentioned Create Master API instead for uploading master. 29 | 30 | ## Version 1.1.0 (3 Feb, 2021) 31 | - Added new Create Asset API (PUT)- `/v2/catalogs/{catalog_id}/assets/{asset_id}` . It can be used to create a new asset with initial metadata and import information. 32 | 33 | - Existing Create Asset Revision API (PUT) - `/v2/catalogs/{catalog_id}/assets/{asset_id}/revisions/{revision_id}` is removed and will be deprecated soon. Please use above mentioned Create Asset API instead for creating asset. 34 | 35 | - Added new Update Album API (POST)- `/v2/catalogs/{catalog_id}/albums/{album_id}`. It can be used to update an existing album. The existing album should be created via the same client app and of subtype project or project_set. 36 | 37 | - Added new Delete Album API (DELETE)- `/v2/catalogs/{catalog_id}/albums/{album_id}`. It can be used to delete an existing album. The existing album should be created via the same client app and of subtype project or project_set. 38 | 39 | - Added new Create External Develop XMP API (PUT)- `/v2/catalogs/{catalog_id}/assets/{asset_id}/xmp/develop`. It can be used to upload external xmp develop file for an asset 40 | 41 | - Added new Read External Develop XMP API (GET)- `/v2/catalogs/{catalog_id}/assets/{asset_id}/xmp/develop`. It can be used to get latest external xmp develop file for an asset -------------------------------------------------------------------------------- /src/pages/support/FAQ/index.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## All questions resolved 4 | 5 | Lorem ipsum -------------------------------------------------------------------------------- /src/pages/support/community/index.md: -------------------------------------------------------------------------------- 1 | # Community 2 | 3 | ## We are here to help 4 | 5 | Lorem ipsum -------------------------------------------------------------------------------- /src/pages/support/contribute/index.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | ## How to contribute ? 4 | 5 | Lorem ipsum -------------------------------------------------------------------------------- /src/pages/support/experience_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdobeDocs/lightroom-public-apis/e3ab0d7a73c1fdfa812314b59fbdcec3625df639/src/pages/support/experience_cloud.png -------------------------------------------------------------------------------- /src/pages/support/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Support - Adobe Lr 3 | description: This is the support page of Adobe Lr 4 | --- 5 | 6 | 7 | 8 | # Support 9 | 10 | Learn where to ask questions, report bugs, make feature requests, and spark discussions. 11 | 12 | ## Feedback 13 | 14 | You've got a place to start discussions. 15 | 16 | 17 | 18 | ![Adobe Experience Cloud](experience_cloud.png) 19 | 20 | ### Developer forum 21 | 22 | [Get started](https://adobe.io) 23 | 24 | Open discussion and support with community experts and Adobe staff. 25 | 26 | 27 | 28 | [Experience league](https://adobe.io) 29 | 30 | Tutorials and videos for the whole community. 31 | 32 | 33 | 34 | [Experience forum](https://adobe.io) 35 | 36 | Forum to get help and help others 37 | 38 | ## Community 39 | 40 | You've got a place to join the conversation. 41 | 42 | 43 | 44 | ![Stack Overflow](stack-overflow.png) 45 | 46 | ### Stack Overflow 47 | 48 | [Explore](https://adobe.io) 49 | 50 | Developer Q&A using a specific tag. 51 | 52 | -------------------------------------------------------------------------------- /src/pages/support/stack-overflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdobeDocs/lightroom-public-apis/e3ab0d7a73c1fdfa812314b59fbdcec3625df639/src/pages/support/stack-overflow.png -------------------------------------------------------------------------------- /static/AutoUploadFromPartnerAppToLR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdobeDocs/lightroom-public-apis/e3ab0d7a73c1fdfa812314b59fbdcec3625df639/static/AutoUploadFromPartnerAppToLR.png -------------------------------------------------------------------------------- /static/BulkUploadOfSelectedAssetsFromPartnerAppToLR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdobeDocs/lightroom-public-apis/e3ab0d7a73c1fdfa812314b59fbdcec3625df639/static/BulkUploadOfSelectedAssetsFromPartnerAppToLR.png -------------------------------------------------------------------------------- /static/ConnectWorkflowDiagrams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdobeDocs/lightroom-public-apis/e3ab0d7a73c1fdfa812314b59fbdcec3625df639/static/ConnectWorkflowDiagrams.png -------------------------------------------------------------------------------- /static/IMS_Authorization_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdobeDocs/lightroom-public-apis/e3ab0d7a73c1fdfa812314b59fbdcec3625df639/static/IMS_Authorization_flow.png -------------------------------------------------------------------------------- /static/OAuthFlowDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdobeDocs/lightroom-public-apis/e3ab0d7a73c1fdfa812314b59fbdcec3625df639/static/OAuthFlowDiagram.png -------------------------------------------------------------------------------- /static/PrintWorkflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdobeDocs/lightroom-public-apis/e3ab0d7a73c1fdfa812314b59fbdcec3625df639/static/PrintWorkflow.png -------------------------------------------------------------------------------- /static/UserLoggedInFromPartnerAppToLR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdobeDocs/lightroom-public-apis/e3ab0d7a73c1fdfa812314b59fbdcec3625df639/static/UserLoggedInFromPartnerAppToLR.png --------------------------------------------------------------------------------