├── .gitignore ├── README.md ├── fmhy-search.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | AdblockVPNGuide.md 2 | AndroidPiracyGuide.md 3 | AudioPiracyGuide.md 4 | base64.md 5 | Beginners-Guide.md 6 | DEVTools.md 7 | DownloadPiracyGuide.md 8 | EDUPiracyGuide.md 9 | Game-Tools.md 10 | GamingPiracyGuide.md 11 | img-tools.md 12 | LinuxGuide.md 13 | MISCGuide.md 14 | Non-English.md 15 | NSFWPiracy.md 16 | ReadingPiracyGuide.md 17 | single-page 18 | STORAGE.md 19 | TOOLSGuide.md 20 | TorrentPiracyGuide.md 21 | VideoPiracyGuide.md 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A basic search engine for [FMHY](https://www.reddit.com/r/FREEMEDIAHECKYEAH/) (a Python script) 2 | It looks like this: 3 | 4 | ![](https://i.imgur.com/xo5e8wi.png) 5 | 6 | ## How to get it working 7 | ### Requirements 8 | 1) You need to have [Python](https://www.python.org/) installed. (When installing on Windows, check "Add to PATH" so that you can easily use the pip command) 9 | 10 | 2) You need to install some Python packages. To check if they are installed, and install the missing ones, type/paste this in the terminal/command-prompt: 11 | ``` 12 | pip install requests termcolor colorama 13 | ``` 14 | ### Usage 15 | Now you can execute the script. Depending on the OS you are in: 16 | - If on Windows, the easiest way is to right click on the script (which is the file [fmhy-search.py](https://github.com/Rust1667/a-FMHY-search-engine/blob/main/fmhy-search.py)) and "Open with" Python. 17 | 18 | - If on Linux, type on the terminal: 19 | ``` 20 | python3 //fmhy-search.py 21 | ``` 22 | 23 | - If on Mac or anything with Python installed, it works too, but I'm not sure what are the exact steps. 24 | 25 | ### Prefer a GUI? 26 | Use https://fmhy-search.streamlit.app/ 27 | It is this same search script, but with a GUI created with Streamlit. 28 | Source code here: https://github.com/Rust1667/fmhy-search-streamlit 29 | 30 | -------------------------------------------------------------------------------- /fmhy-search.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | #enable text coloring only if the requirements are met 4 | coloring = True 5 | try: 6 | from termcolor import colored 7 | import colorama 8 | colorama.init() 9 | except: 10 | coloring = False 11 | 12 | 13 | #----------------Alt Indexing------------ 14 | doAltIndexing = True 15 | 16 | def addPretext(lines, icon, baseURL, subURL): 17 | modified_lines = [] 18 | currMdSubheading = "" 19 | currSubCat = "" 20 | currSubSubCat = "" 21 | 22 | for line in lines: 23 | if line.startswith("#"): #Title Lines 24 | if not subURL=="storage": 25 | if line.startswith("# ►"): 26 | currMdSubheading = "#" + line.replace("# ►", "").strip().replace(" / ", "-").replace(" ", "-").lower() 27 | currSubCat = "/ " + line.replace("# ►", "").strip() + " " 28 | currSubSubCat = "" 29 | elif line.startswith("## ▷"): 30 | if not subURL=="non-english": #Because non-eng section has multiple subsubcats with same names 31 | currMdSubheading = "#" + line.replace("## ▷", "").strip().replace(" / ", "-").replace(" ", "-").lower() 32 | currSubSubCat = "/ " + line.replace("## ▷", "").strip() + " " 33 | elif subURL=="storage": 34 | if line.startswith("## "): 35 | currMdSubheading = "#" + line.replace("## ", "").strip().replace(" / ", "-").replace(" ", "-").lower() 36 | currSubCat = "/ " + line.replace("## ", "").strip() + " " 37 | currSubSubCat = "" 38 | elif line.startswith("### "): 39 | currMdSubheading = "#" + line.replace("### ", "").strip().replace(" / ", "-").replace(" ", "-").lower() 40 | currSubSubCat = "/ " + line.replace("### ", "").strip() + " " 41 | 42 | # Remove links from subcategory titles (because the screw the format) 43 | if 'http' in currSubCat: currSubCat = '' 44 | if 'http' in currSubSubCat: currSubSubCat = '' 45 | 46 | elif any(char.isalpha() for char in line): #If line has content 47 | preText = f"[{icon}{currSubCat}{currSubSubCat}]({baseURL}{subURL}{currMdSubheading}) ► " 48 | if line.startswith("* "): line = line[2:] 49 | modified_lines.append(preText + line) 50 | 51 | return modified_lines 52 | 53 | 54 | #----------------base64 page processing------------ 55 | import base64 56 | import re 57 | 58 | doBase64Decoding = True 59 | 60 | def fix_base64_string(encoded_string): 61 | missing_padding = len(encoded_string) % 4 62 | if missing_padding != 0: 63 | encoded_string += '=' * (4 - missing_padding) 64 | return encoded_string 65 | 66 | def decode_base64_in_backticks(input_string): 67 | def base64_decode(match): 68 | encoded_data = match.group(0)[1:-1] # Extract content within backticks 69 | decoded_bytes = base64.b64decode( fix_base64_string(encoded_data) ) 70 | try: 71 | return decoded_bytes.decode() 72 | except: 73 | print(f"Failed to decode base64 string: {encoded_data}") 74 | return encoded_data 75 | 76 | pattern = r"`[^`]+`" # Regex pattern to find substrings within backticks 77 | decoded_string = re.sub(pattern, base64_decode, input_string) 78 | return decoded_string 79 | 80 | def remove_empty_lines(text): 81 | lines = text.split('\n') # Split the text into lines 82 | non_empty_lines = [line for line in lines if line.strip()] # Filter out empty lines 83 | return '\n'.join(non_empty_lines) # Join non-empty lines back together 84 | 85 | def extract_base64_sections(base64_page): 86 | sections = base64_page.split("***") # Split the input string by "***" to get sections 87 | formatted_sections = [] 88 | for section in sections: 89 | formatted_section = remove_empty_lines( section.strip().replace("#### ", "").replace("\n\n", " - ").replace("\n", ", ") ) 90 | if doBase64Decoding: formatted_section = decode_base64_in_backticks(formatted_section) 91 | formatted_section = '[🔑Base64](https://rentry.co/FMHYBase64) ► ' + formatted_section 92 | formatted_sections.append(formatted_section) 93 | lines = formatted_sections 94 | return lines 95 | #----------------base64 page processing------------ 96 | 97 | 98 | 99 | def dlWikiChunk(fileName, icon, redditSubURL): 100 | 101 | #first, try to get the chunk locally 102 | try: 103 | #First, try to get it from the local file 104 | print("Loading " + fileName + " from local file...") 105 | with open(fileName.lower(), 'r') as f: 106 | page = f.read() 107 | print("Loaded.\n") 108 | #if not available locally, download the chunk 109 | except: 110 | if fileName=='NSFWPiracy.md': 111 | print("Local file not found. Downloading rentry.co/freemediafuckyeah...") 112 | page = requests.get("https://rentry.co/freemediafuckyeah/raw").text.replace("\r", "") 113 | elif not fileName=='base64.md': 114 | print("Local file not found. Downloading " + fileName + " from Github...") 115 | page = requests.get("https://raw.githubusercontent.com/fmhy/FMHYedit/main/docs/" + fileName.lower()).text 116 | elif fileName=='base64.md': 117 | print("Local file not found. Downloading rentry.co/FMHYBase64...") 118 | page = requests.get("https://rentry.co/FMHYBase64/raw").text.replace("\r", "") 119 | print("Downloaded") 120 | 121 | #add a pretext 122 | redditBaseURL = "https://www.reddit.com/r/FREEMEDIAHECKYEAH/wiki/" 123 | siteBaseURL = "https://fmhy.net/" 124 | if not fileName=='base64.md': 125 | pagesDevSiteSubURL = fileName.replace(".md", "").lower() 126 | subURL = pagesDevSiteSubURL 127 | lines = page.split('\n') 128 | lines = addPretext(lines, icon, siteBaseURL, subURL) 129 | elif fileName=='base64.md': 130 | lines = extract_base64_sections(page) 131 | 132 | return lines 133 | 134 | def cleanLineForSearchMatchChecks(line): 135 | siteBaseURL = "https://fmhy.net/" 136 | redditBaseURL = "https://www.reddit.com/r/FREEMEDIAHECKYEAH/wiki/" 137 | return line.replace(redditBaseURL, '/').replace(siteBaseURL, '/') 138 | 139 | def alternativeWikiIndexing(): 140 | wikiChunks = [ 141 | dlWikiChunk("VideoPiracyGuide.md", "📺", "video"), 142 | dlWikiChunk("AI.md", "🤖", "ai"), 143 | dlWikiChunk("Android-iOSGuide.md", "📱", "android"), 144 | dlWikiChunk("AudioPiracyGuide.md", "🎵", "audio"), 145 | dlWikiChunk("DownloadPiracyGuide.md", "💾", "download"), 146 | dlWikiChunk("EDUPiracyGuide.md", "🧠", "edu"), 147 | dlWikiChunk("GamingPiracyGuide.md", "🎮", "games"), 148 | dlWikiChunk("AdblockVPNGuide.md", "📛", "adblock-vpn-privacy"), 149 | dlWikiChunk("System-Tools.md", "💻", "system-tools"), 150 | dlWikiChunk("File-Tools.md", "🗃️", "file-tools"), 151 | dlWikiChunk("Internet-Tools.md", "🔗", "internet-tools"), 152 | dlWikiChunk("Social-Media-Tools.md", "💬", "social-media"), 153 | dlWikiChunk("Text-Tools.md", "📝", "text-tools"), 154 | dlWikiChunk("Video-Tools.md", "📼", "video-tools"), 155 | dlWikiChunk("MISCGuide.md", "📂", "misc"), 156 | dlWikiChunk("ReadingPiracyGuide.md", "📗", "reading"), 157 | dlWikiChunk("TorrentPiracyGuide.md", "🌀", "torrent"), 158 | dlWikiChunk("img-tools.md", "📷", "img-tools"), 159 | dlWikiChunk("gaming-tools.md", "👾", "gaming-tools"), 160 | dlWikiChunk("LinuxGuide.md", "🐧🍏", "linux"), 161 | dlWikiChunk("DEVTools.md", "🖥️", "dev-tools"), 162 | dlWikiChunk("Non-English.md", "🌏", "non-eng"), 163 | dlWikiChunk("STORAGE.md", "🗄️", "storage"), 164 | dlWikiChunk("base64.md", "🔑", "base64"), 165 | dlWikiChunk("NSFWPiracy.md", "🌶", "https://saidit.net/s/freemediafuckyeah/wiki/index") 166 | ] 167 | return [item for sublist in wikiChunks for item in sublist] #Flatten a into a 168 | #-------------------------------- 169 | 170 | 171 | def standardWikiIndexing(): 172 | try: 173 | #First, try to get it from the local single-page file 174 | print("Loading FMHY from local single-page...") 175 | with open('single-page', 'r') as f: 176 | data = f.read() 177 | print("Loaded.\n") 178 | except: 179 | print("Local single-page file not found.") 180 | #If that fails, try to get it from Github 181 | print("Loading FMHY single-page file from Github...") 182 | response1 = requests.get("https://api.fmhy.net/single-page") 183 | print("Loaded.\n") 184 | data = response1.text 185 | lines = data.split('\n') 186 | return lines 187 | 188 | def getAllLines(): 189 | return alternativeWikiIndexing() 190 | 191 | def removeEmptyStringsFromList(stringList): 192 | return [string for string in stringList if string != ''] 193 | 194 | def checkMultiWordQueryContainedExactlyInLine(line, searchQuery): 195 | if len(searchQuery.split(' ')) <= 1: 196 | return False 197 | return (searchQuery.lower() in line.lower()) 198 | 199 | def moveExactMatchesToFront(myList, searchQuery): 200 | bumped = [] 201 | notBumped = [] 202 | for element in myList: 203 | if checkMultiWordQueryContainedExactlyInLine(element, searchQuery): 204 | bumped.append(element) 205 | else: 206 | notBumped.append(element) 207 | return (bumped + notBumped) 208 | 209 | def checkList1isInList2(list1, list2): 210 | for element in list1: 211 | if element not in list2: 212 | return False 213 | return True 214 | 215 | def checkWordForWordMatch(line, searchQuery): 216 | lineWords = removeEmptyStringsFromList( line.lower().replace('[', ' ').replace(']', ' ').split(' ') ) 217 | lineWords = [element.strip() for element in lineWords] #doesnt work on streamlit without this line even though it works locally 218 | searchQueryWords = removeEmptyStringsFromList( searchQuery.lower().split(' ') ) 219 | return checkList1isInList2(searchQueryWords, lineWords) 220 | 221 | def checkWordForWordMatchCaseSensitive(line, searchQuery): 222 | lineWords = removeEmptyStringsFromList( line.replace('[', ' ').replace(']', ' ').split(' ') ) 223 | lineWords = [element.strip() for element in lineWords] #doesnt work on streamlit without this line even though it works locally 224 | searchQueryWords = removeEmptyStringsFromList( searchQuery.split(' ') ) 225 | return checkList1isInList2(searchQueryWords, lineWords) 226 | 227 | def moveBetterMatchesToFront(myList, searchQuery): 228 | bumped = [] 229 | notBumped = [] 230 | for element in myList: 231 | if checkWordForWordMatch(element, searchQuery): 232 | bumped.append(element) 233 | else: 234 | notBumped.append(element) 235 | return (bumped + notBumped) 236 | 237 | def getOnlyFullWordMatches(myList, searchQuery): 238 | bumped = [] 239 | for element in myList: 240 | if checkWordForWordMatch(element, searchQuery): 241 | bumped.append(element) 242 | return bumped 243 | 244 | def getOnlyFullWordMatchesCaseSensitive(myList, searchQuery): 245 | bumped = [] 246 | for element in myList: 247 | if checkWordForWordMatchCaseSensitive(element, searchQuery): 248 | bumped.append(element) 249 | return bumped 250 | 251 | def getLinesThatContainAllWords(lineList, searchQuery): 252 | words = removeEmptyStringsFromList( searchQuery.lower().split(' ') ) 253 | bumped = [] 254 | for line in lineList: 255 | if doAltIndexing: 256 | lineModdedForChecking = cleanLineForSearchMatchChecks(line).lower() 257 | else: 258 | lineModdedForChecking = line.lower() 259 | for word in words: 260 | if word not in lineModdedForChecking: 261 | break 262 | else: 263 | bumped.append(line) 264 | return bumped 265 | 266 | def filterLines(lineList, searchQuery): 267 | if len(searchQuery)<=2 or (searchQuery==searchQuery.upper() and len(searchQuery)<=5): 268 | return getOnlyFullWordMatches(lineList, searchQuery) 269 | else: 270 | return getLinesThatContainAllWords(lineList, searchQuery) 271 | 272 | def filterOutTitleLines(lineList): 273 | filteredList = [] 274 | sectionTitleList = [] 275 | for line in lineList: 276 | if line[0] != "#": 277 | filteredList.append(line) 278 | else: 279 | sectionTitleList.append(line) 280 | return [filteredList, sectionTitleList] 281 | 282 | 283 | def highlightWord(sentence, word): 284 | return sentence.replace(word, colored(word,'red')) 285 | 286 | def colorLinesFound(linesFound, filterWords): 287 | filterWordsCapitalized=[] 288 | for word in filterWords: 289 | filterWordsCapitalized.append(word.capitalize()) 290 | 291 | filterWordsAllCaps=[] 292 | for word in filterWords: 293 | filterWordsAllCaps.append(word.upper()) 294 | 295 | filterWordsIncludingCaps = filterWords + filterWordsCapitalized + filterWordsAllCaps 296 | coloredLinesList = [] 297 | for line in linesFound: 298 | for word in filterWordsIncludingCaps: 299 | line = highlightWord(line, word) 300 | coloredLine = line 301 | coloredLinesList.append(coloredLine) 302 | return coloredLinesList 303 | 304 | def addNumberingToStringList(string_list): 305 | for i in range(len(string_list)): 306 | string_list[i] = f"{i + 1}- {string_list[i]}" 307 | return string_list 308 | 309 | def doASearch(searchInput): 310 | 311 | #intro to the search results 312 | myFilterWords = removeEmptyStringsFromList( searchInput.lower().split(' ') ) 313 | print("Looking for lines that contain all of these words:") 314 | print(myFilterWords) 315 | 316 | #main results 317 | myLineList = lineList 318 | linesFoundPrev = filterLines(myLineList, searchInput) 319 | 320 | #limit result list 321 | if len(linesFoundPrev) > 300: 322 | print("Too many results (" + str(len(linesFoundPrev)) + "). Showing only full-word matches.") 323 | linesFoundPrev = getOnlyFullWordMatches(linesFoundPrev, searchInput) 324 | 325 | #rank results 326 | #linesFoundPrev = moveExactMatchesToFront(linesFoundPrev, searchInput) 327 | linesFoundPrev = moveBetterMatchesToFront(linesFoundPrev, searchInput) 328 | 329 | #separate title lines 330 | linesFoundAll = filterOutTitleLines(linesFoundPrev) 331 | linesFound = linesFoundAll[0] 332 | linesFound = addNumberingToStringList(linesFound) 333 | sectionTitleList = linesFoundAll[1] 334 | 335 | #reverse list for terminal 336 | linesFound.reverse() 337 | 338 | #check for coloring 339 | if coloring == True: 340 | linesFoundColored = colorLinesFound(linesFound, myFilterWords) 341 | textToPrint = "\n\n".join(linesFoundColored) 342 | else: 343 | textToPrint = "\n\n".join(linesFound) 344 | 345 | # print main results 346 | print("Printing " + str(len(linesFound)) + " search results:\n") 347 | print(textToPrint) 348 | print("\nSearch ended with " + str(len(linesFound)) + " results found.\n") 349 | 350 | #title section results 351 | if len(sectionTitleList)>0: 352 | print("Also there are these section titles: ") 353 | print("\n".join(sectionTitleList)) 354 | 355 | 356 | 357 | def searchLoop(): 358 | print("STARTING NEW SEARCH...\n") 359 | 360 | searchInput = input("Type a search string: ") 361 | if searchInput == "exit" or searchInput == "q" or searchInput == "": 362 | print("The script is closing...") 363 | return 364 | 365 | doASearch(searchInput.strip()) 366 | print("\n\n\n") 367 | searchLoop() 368 | 369 | 370 | 371 | lineList = getAllLines() 372 | print("Search examples: 'youtube frontend', 'streaming site', 'rare movies', 'userscripts'... You can also type 'exit' or nothing to close the script.\n") 373 | searchLoop() 374 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | termcolor 3 | colorama 4 | --------------------------------------------------------------------------------