Skip to content

Instantly share code, notes, and snippets.

@marccarre
Created October 24, 2020 08:46
Show Gist options
  • Save marccarre/645fe68da31678f9191cd3aafecfea1b to your computer and use it in GitHub Desktop.
Save marccarre/645fe68da31678f9191cd3aafecfea1b to your computer and use it in GitHub Desktop.

Revisions

  1. marccarre created this gist Oct 24, 2020.
    82 changes: 82 additions & 0 deletions list_kindle_releases.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,82 @@
    #!/usr/bin/env python
    '''
    List all available versions of Kindle for Mac and Kindle for PC.
    Dependencies:
    - asyncio==3.4.3
    - aiohttp==3.6.3
    '''

    import os
    import sys
    import asyncio
    import aiohttp


    MAX_ATTEMPTS = int(os.environ.get('MAX_ATTEMPTS', 3)) # Maximum number of attempts per URL.
    RATE_LIMIT = int(os.environ.get('RATE_LIMIT', 64)) # Number of active requests at any given time.


    '''
    Build numbers (5XXXX) increment:
    - by 1000 for minor versions
    - by 1 for patch versions -- with gaps, as some builds are not released.
    '''
    VERSIONS = [
    {'build': 55000, 'version': '1.26'},
    {'build': 56000, 'version': '1.27'},
    {'build': 57000, 'version': '1.28'},
    {'build': 58000, 'version': '1.29'},
    {'build': 59000, 'version': '1.30'},
    ]


    '''
    For example:
    - Kindle 1.26
    - https://s3.amazonaws.com/kindleforpc/55076/KindleForPC-installer-1.26.55076.exe
    - https://s3.amazonaws.com/kindleformac/55093/KindleForMac-55093.dmg
    - Kindle 1.30
    - https://s3.amazonaws.com/kindleforpc/59056/KindleForPC-installer-1.30.59056.exe
    - https://s3.amazonaws.com/kindleformac/59055/KindleForMac-1.30.59055.dmg
    '''
    URL_PATTERNS = [
    'https://s3.amazonaws.com/kindleforpc/{build}/KindleForPC-installer-{version}.{build}.exe',
    'https://s3.amazonaws.com/kindleformac/{build}/KindleForMac-{build}.dmg',
    'https://s3.amazonaws.com/kindleformac/{build}/KindleForMac-{version}.{build}.dmg',
    ]


    async def main():
    async with aiohttp.ClientSession() as session:
    urls = await fetch_all(session)
    print('\n'.join(sorted([url for url in urls if url])))


    async def fetch_all(session):
    tasks = []
    semaphore = asyncio.Semaphore(RATE_LIMIT)
    for v in VERSIONS:
    for build in range(v['build'], v['build'] + 1000):
    for pattern in URL_PATTERNS:
    url = pattern.format(build=build, version=v['version'])
    task = asyncio.create_task(fetch(session, semaphore, url))
    tasks.append(task)
    results = await asyncio.gather(*tasks)
    return results


    async def fetch(session, semaphore, url):
    for attempt in range(1, MAX_ATTEMPTS + 1):
    try:
    async with semaphore:
    response = await session.head(url)
    return url if response.status == 200 else None
    except Exception as e:
    if attempt < MAX_ATTEMPTS:
    print('Attempt #%d: Retrying on %s as got: %s.' % (attempt, url, e), file=sys.stderr)
    else:
    print('Failed on %s with: %s' % (url, e), file=sys.stderr)


    if __name__ == '__main__':
    asyncio.run(main())