├── requirements.txt ├── .gitignore ├── images ├── Line_sticker_-_ID_from_app.png ├── Line_sticker_-_search_by_ID.png ├── Line_sticker_-_ID_share_button.png └── Line_sticker_-_search_by_name.png ├── .github └── workflows │ └── python-package.yml ├── sticker_dl_test.py ├── README.md └── sticker_dl.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .vscode/ -------------------------------------------------------------------------------- /images/Line_sticker_-_ID_from_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doubleplusc/Line-sticker-downloader/HEAD/images/Line_sticker_-_ID_from_app.png -------------------------------------------------------------------------------- /images/Line_sticker_-_search_by_ID.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doubleplusc/Line-sticker-downloader/HEAD/images/Line_sticker_-_search_by_ID.png -------------------------------------------------------------------------------- /images/Line_sticker_-_ID_share_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doubleplusc/Line-sticker-downloader/HEAD/images/Line_sticker_-_ID_share_button.png -------------------------------------------------------------------------------- /images/Line_sticker_-_search_by_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doubleplusc/Line-sticker-downloader/HEAD/images/Line_sticker_-_search_by_name.png -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: [3.7, 3.8, 3.9] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 pytest 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 37 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 38 | - name: Test with pytest 39 | run: | 40 | pytest 41 | -------------------------------------------------------------------------------- /sticker_dl_test.py: -------------------------------------------------------------------------------- 1 | import sticker_dl 2 | 3 | import re 4 | import codecs 5 | 6 | ESCAPE_SEQUENCE_RE = re.compile(r''' 7 | ( \\U........ # 8-digit hex escapes 8 | | \\u.... # 4-digit hex escapes 9 | | \\x.. # 2-digit hex escapes 10 | | \\[0-7]{1,3} # Octal escapes 11 | | \\N\{[^}]+\} # Unicode characters by name 12 | | \\[\\'"abfnrtv] # Single-character escapes 13 | )''', re.UNICODE | re.VERBOSE) 14 | 15 | 16 | def decode_escapes(s): 17 | def decode_match(match): 18 | return codecs.decode(match.group(0), 'unicode-escape') 19 | 20 | return ESCAPE_SEQUENCE_RE.sub(decode_match, s) 21 | 22 | 23 | def main(): 24 | 25 | # http://stackoverflow.com/questions/4020539/process-escape-sequences-in-a-string-in-python 26 | # 2 hours of searching. Python why. 27 | pack_meta = sticker_dl.get_pack_meta(5737).text 28 | name_string = """"ko":""" 29 | pack_name = sticker_dl.get_pack_name(name_string, pack_meta) 30 | decoded_name = decode_escapes(pack_name) 31 | file_name = "".join(i for i in decoded_name if i not in r'\/:*?"<>|') # https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words # noqa: E501 32 | print(file_name) 33 | 34 | pack_meta = sticker_dl.get_pack_meta(5737).text 35 | name_string = """"en":""" 36 | pack_name = sticker_dl.get_pack_name(name_string, pack_meta) 37 | decoded_name = decode_escapes(pack_name) 38 | file_name = "".join(i for i in decoded_name if i not in r'\/:*?"<>|') 39 | print(file_name) 40 | 41 | 42 | if __name__ == '__main__': 43 | main() 44 | 45 | 46 | def test_dummy(): 47 | assert 0 == 0 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Line sticker downloader 2 | This is a Python script that downloads Line stickers, both static and animated (where available). 3 | 4 | For Windows 7 64-bit users, a standalone executable is available on the releases page. Its compatibility with Windows 8 and 10 are not guaranteed. For other operating systems, please consult https://docs.python.org/3.5/using/index.html for installing and using Python. 5 | 6 | Many Line stickers are unavailable due to geographic restrictions. For example, the popular sticker set gudetama is available in different countries with different language captions. Users in America are only able to buy the gudetama pack with English text, and users in Taiwan are only able to buy the gudetama pack with Chinese text. Additionally, limited time/expired/promotional sticker packs will no longer be available for use after their offering periods. 7 | 8 | With this program, users can download stickers from any region and from expired packs. Downloads are in png and gif form, users are free to use the stickers in applications outside of Line. NOTE: Animated "popup" stickers with sound are not currently supported. This will be fixed in the next version. 9 | 10 | There are some limitations to this program that are beyond my control: 11 | 12 | 1. Line will only support static images through the photo upload feature, even if the sticker is animated. Line does not support inline animated images, unlike programs like Wechat. 13 | 14 | 2. You have to directly transfer the downloaded stickers from your computer to your phone. There are plenty of programs out there that make device transfers less painful. 15 | 16 | Your stickers will be downloaded to a folder on the same level as where your script is running. 17 | 18 | If your script is running from C:\Users\Foo\sticker_dl.py, and you download a sticker pack named Bar, your downloaded stickers will be in C:\Users\Foo\Bar\. 19 | 20 | # Finding the ID 21 | 22 | To search for stickers by name, try http://www.line-stickers.com/. 23 | 24 | The search result will link to a page that contains the ID. 25 | 26 | This site is not necessarily up to date, and you may need to search by release date and ID on http://lstk.ddns.net/Stickers.php. 27 | 28 | ![](/images/Line_sticker_-_search_by_name.png?raw=true) 29 | 30 | You can also find the ID through your Line app's sticker preview page and pressing the Share button. If you share to a program that transmits text, the ID and link will be pasted. (Note that your mobile app's page only contains region-locked and unexpired results.) 31 | 32 | ![](/images/Line_sticker_-_ID_share_button.png?raw=true) 33 | 34 | ![](/images/Line_sticker_-_ID_from_app.png?raw=true) 35 | 36 | After getting the ID, you can get a preview of the stickers and animated versions where available on http://lstk.ddns.net/Stickers.php. This site is also handy for browsing the entire Line archive of available stickers, most recent shown first. Click on the thumbnails to load the preview page. Packs with animations require clicking on the static images to load. 37 | 38 | ![](/images/Line_sticker_-_search_by_ID.png?raw=true) 39 | 40 | # Using the Program using python directly 41 | 42 | to run 43 | 44 | `python sticker_dl.py` 45 | 46 | if you find this not working properly please provide your python version 47 | 48 | Check python version 49 | 50 | `python -V` 51 | 52 | # Using the Program 53 | When you open the program, you will be prompted 54 | 55 | ```Enter the sticker pack ID:``` 56 | 57 | Enter the ID you found from the previous step. 58 | 59 | You will be notified whether animated stickers are available, and prompted for your desired download. 60 | 61 | # Download options 62 | png: static stickers only. 63 | 64 | y: static stickers only. 65 | 66 | gif: animated stickers only. 67 | 68 | both: both static and animated stickers will be downloaded. 69 | 70 | Anything else: exit the program without downloading. 71 | 72 | 73 | # TODO 74 | 75 | Next release: support animated "popup" stickers. Script only downloads static versions for now. 76 | 77 | Allow program to take input from a text file for batch downloading. 78 | 79 | Add a GUI to make program less offensive looking. 80 | 81 | Allow inline previewing of image? 82 | 83 | Line store seems to have .apng files, an uncommon format. Should add option for that. 84 | 85 | Unlikely, but on the wishlist: inline search of sticker packs, removing need to separately go on other sites to find ID; database for sticker descriptions? Also figure out how sound stickers are implemented. 86 | 87 | 88 | 89 | DISCLAIMER: I take no responsibility for what users do with this program. 90 | -------------------------------------------------------------------------------- /sticker_dl.py: -------------------------------------------------------------------------------- 1 | '''Line sticker downloader''' 2 | 3 | 4 | import requests 5 | import sys 6 | import os 7 | import re 8 | import codecs 9 | 10 | 11 | def main(): 12 | pack_ext = "" 13 | 14 | if len(sys.argv) > 1: 15 | pack_id = int(sys.argv[1]) 16 | if len(sys.argv) > 2: 17 | pack_ext = sys.argv[2] 18 | else: 19 | pack_id = int(input("Enter the sticker pack ID: ")) 20 | pack_meta = get_pack_meta(pack_id).text 21 | 22 | name_string = """"en":""" # folder name will take pack's English title 23 | pack_name = get_pack_name(name_string, pack_meta) 24 | pack_name = decode_escapes(pack_name) 25 | pack_name = pack_name.strip() # To remove empty sides spaces # Example Bug: Sticker ID= 9721 Name= UNIVERSTAR BT21: Cuteness Overloaded! # noqa: E501 26 | print("\nThis pack contains stickers for", pack_name) 27 | 28 | if pack_ext == "": 29 | if """"hasAnimation":true""" in pack_meta: 30 | if sys.version_info[0] < 3: 31 | # https://stackoverflow.com/questions/31722883/python-nameerror-name-hello-is-not-defined 32 | # compatibility python v2 33 | pack_ext = input("\nAnimated stickers available! \n" 34 | "Enter png, apng, or both, anything else to exit: ") # noqa: E501 35 | else: 36 | pack_ext = input("\nAnimated stickers available! \n" 37 | "Enter png, apng, or both, anything else to exit: ") # noqa: E501 38 | 39 | else: 40 | if sys.version_info[0] < 3: 41 | # https://stackoverflow.com/questions/31722883/python-nameerror-name-hello-is-not-defined 42 | # compatibility python v2 43 | pack_ext = input("\nOnly static stickers available! \n" 44 | "y to download, anything else to exit: ") 45 | else: 46 | pack_ext = input("\nOnly static stickers available! \n" 47 | "y to download, anything else to exit: ") 48 | 49 | id_string = """"id":""" 50 | list_ids = [] 51 | 52 | current_id, start_index = 0, 0 # [4] Why have start_index included 53 | 54 | while start_index != -1: 55 | start_index, current_id, pack_meta = get_ids(id_string, pack_meta) 56 | # "Passing by assignment" mutable vs. immutable. Any reassignments done in called function will not reflect on return. But manipulating the parameter will reflect. # noqa: E501 57 | list_ids.append(current_id) 58 | 59 | list_ids.pop() # [4] Why pop 60 | 61 | # [3] A less ugly way of checking menu values 62 | menu = {'apng': (get_gif,), 63 | 'png': (get_png,), 64 | 'y': (get_png,), 65 | 'both': (get_gif, get_png)} # D'OH! Originally said tuples wouldn't work, which was strange. Thanks to doing MIT problems, I realized I used (var) instead of (var,). Former will not be considered a tuple. # noqa: E501 66 | if pack_ext in menu: 67 | for choice in menu[pack_ext]: 68 | choice(pack_id, list_ids, pack_name) 69 | else: 70 | print("Nothing done. Program exiting...") 71 | sys.exit() 72 | 73 | print("\nDone! Program exiting...") 74 | 75 | sys.exit() 76 | 77 | 78 | def get_pack_name(name_string, pack_meta): 79 | start_index = pack_meta.find(name_string) 80 | end_index = pack_meta.find(',', start_index + 1) 81 | sticker_name = pack_meta[start_index+len(name_string)+1:end_index-1] # lower bound needs +1 to exclude the beginning " mark. -1 to make upper bound the , which is excluded from the range # noqa: E501 82 | return sticker_name 83 | 84 | 85 | def get_ids(id_string, pack_meta): 86 | start_index = pack_meta.find(id_string) 87 | end_index = pack_meta.find(",", start_index + 1) 88 | sticker_id = pack_meta[start_index+len(id_string):end_index] 89 | return start_index, sticker_id, pack_meta[end_index:] 90 | 91 | 92 | def validate_savepath(pack_name): 93 | decoded_name = decode_escapes(pack_name) 94 | save_name = "".join(i for i in decoded_name if i not in r'\/:*?"<>|') 95 | 96 | # python version selection 97 | if sys.version_info[0] < 3: 98 | # https://github.com/bamos/dcgan-completion.tensorflow/issues/20 99 | # compatibility python v2 100 | try: 101 | os.makedirs(str(save_name)) 102 | except OSError: 103 | print("Skipping creation of %s because it exists already." % str(save_name)) # noqa: E501 104 | else: 105 | # python version >= 3 106 | os.makedirs(str(save_name), exist_ok=True) # exist_ok = True doesn't raise exception if directory exists. Files already in directory are not erased # noqa: E501 107 | 108 | return save_name 109 | 110 | 111 | def get_gif(pack_id, list_ids, pack_name): 112 | pack_name = validate_savepath(pack_name) 113 | for x in list_ids: 114 | # save_path = os.path.join(str(pack_name), str(x) + '.gif') 115 | save_path = os.path.join(str(pack_name), str(x) + '.apng') 116 | # url = 'http://lstk.ddns.net/animg/{}.gif'.format(x) 117 | url = 'https://sdl-stickershop.line.naver.jp/products/0/0/1/{}/iphone/animation/{}@2x.png'.format(pack_id, x) # noqa: E501 118 | image = requests.get(url, stream=True) 119 | with open(save_path, 'wb') as f: 120 | for chunk in image.iter_content(chunk_size=10240): 121 | if chunk: 122 | f.write(chunk) 123 | 124 | 125 | def get_png(pack_id, list_ids, pack_name): 126 | pack_name = validate_savepath(pack_name) 127 | for x in list_ids: 128 | save_path = os.path.join(str(pack_name), str(x) + '.png') 129 | url = 'http://dl.stickershop.line.naver.jp/stickershop/v1/sticker/{}/iphone/sticker@2x.png'.format(x) # noqa: E501 130 | image = requests.get(url, stream=True) 131 | with open(save_path, 'wb') as f: # http://stackoverflow.com/questions/16694907/how-to-download-large-file-in-python-with-requests-py Understood! with construct is a fancy way of try/catch that cleans up, even with exceptions thrown # noqa: E501 132 | for chunk in image.iter_content(chunk_size=10240): # chunk_size is in bytes # noqa: E501 133 | if chunk: 134 | f.write(chunk) 135 | 136 | 137 | def get_pack_meta(pack_id): 138 | 139 | pack_url = "http://dl.stickershop.line.naver.jp/products/0/0/1/{}/android/productInfo.meta".format(pack_id) # noqa: E501 140 | pack_meta = requests.get(pack_url) 141 | 142 | # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html Status codes 143 | # It seems that normal request gives 200. Not sure what it means for program if non200 code is given. Will work with 200 for now. # noqa: E501 144 | 145 | if pack_meta.status_code == 200: 146 | return pack_meta 147 | else: 148 | print("{} did not return 200 status code, possibly invalid sticker ID. Program exiting...".format(pack_id)) # noqa: E501 149 | sys.exit() 150 | 151 | 152 | unicode_sanitizer = re.compile(r''' # compile pattern into object, use with match() 153 | ( \\U........ # 8-digit hex escapes, backslash U followed by 8 non-newline characters # noqa: E501 154 | | \\u.... # 4-digit hex escapes, bksl u followed by 4 non-newline characters # noqa: E501 155 | | \\x.. # 2-digit hex escapes, bksl x followed by 2 non-newline characters # noqa: E501 156 | | \\[0-7]{1,3} # Octal escapes, bksl followed by 1 to 3 numbers within range of 0-7 # noqa: E501 157 | | \\N\{[^}]+\} # Unicode characters by name, uses name index 158 | | \\[\\'"abfnrtv] # Single-character escapes, e.g. tab, backspace, quotes 159 | )''', re.VERBOSE) # re.UNICODE not necessary in Py3, matches Unicode by default. re.VERBOSE allows separated sections # noqa: E501 160 | 161 | 162 | def decode_escapes(orig): 163 | def decode_match(match): 164 | return codecs.decode(match.group(0), 'unicode-escape') 165 | return unicode_sanitizer.sub(decode_match, orig) # sub returns string with replaced patterns # noqa: E501 166 | 167 | 168 | if __name__ == '__main__': 169 | main() 170 | 171 | 172 | ''' 173 | [1] http://stackoverflow.com/questions/11435331/python-requests-and-unicode 174 | Solve Unicode with r.content instead of r.text 175 | [2] w+ creates file if it doesn't exist, truncates if it exists. b is for binary, Windows is picky. Never hurts to add b for platform friendliness 176 | [3] http://stackoverflow.com/questions/3260057/how-to-check-variable-against-2-possible-values-python 177 | leads to http://stackoverflow.com/questions/13186542/functions-in-python-dictionary 178 | For multiple functions per key: http://stackoverflow.com/questions/9205081/python-is-there-a-way-to-store-a-function-in-a-list-or-dictionary-so-that-when 179 | How clever. 180 | http://stackoverflow.com/a/9139961 If I didn't have a check for key in dict, this would've been another way. 181 | [4] Originally had a conditional in the while state to check if the start_index was -1 to make sure it doesn't get added. 182 | But a single pop at the end is much better than the if check in every loop iteration. 183 | [5] http://stackoverflow.com/questions/4020539/process-escape-sequences-in-a-string-in-python 184 | Regular expression saves the day. 185 | ''' # noqa: E501 186 | 187 | ''' 188 | putting "# nopep8" (or "# noqa") on the last line of a multiline string disables all physical checks for lines in the string 189 | https://github.com/PyCQA/pycodestyle/pull/243 190 | ''' 191 | --------------------------------------------------------------------------------