Nimbin2git.christianimmanuel.de / Games / dvd / imdb_search.c

dvd files

last change 2026-09-04 (2 hours ago)

wget https://git.christianimmanuel.de/games/dvd/archive/dvd.tar.gz
imdb_search.c 22.2 KB · 639 lines raw
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "imdb_search.h"

#define MAX_ENTRIES 15
#define NUM_THREADS 16

#define PAGE_SIZE 6

// ANSI color codes
#define RESET   "\033[0m"
#define RED     "\033[31m"
#define GREEN   "\033[32m"
#define YELLOW  "\033[33m"
#define BLUE    "\033[34m"
#define MAGENTA "\033[35m"
#define CYAN    "\033[36m"
#define WHITE   "\033[37m"

#define PRINT_HEADER(title)       \
    printf(MAGENTA "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" RESET); \
    printf(CYAN "      %s\n" RESET, title);   \
    printf(MAGENTA "┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅\n" RESET);

#define PRINT_HEADER_SMALL(title)       \
    printf(MAGENTA "\n–––––––––––––––––––––––––––––––––––––––––\n" RESET); \
    printf(CYAN "      %s\n" RESET, title);   \


void trim_whitespace(char *str) {
    char *start = str;
    char *end;

    // Trim leading whitespace
    while (isspace((unsigned char)*start)) {
        start++;
    }

    // Shift the trimmed string to the beginning if necessary
    if (start != str) {
        memmove(str, start, strlen(start) + 1);
    }

    // Find the end of the string
    end = str + strlen(str) - 1;

    // Trim trailing whitespace
    while (end > str && isspace((unsigned char)*end)) {
        *end = '\0';
        end--;
    }
}



// Utility function to convert string to lowercase
void to_lowercase(char *p) {
    for ( ; *p; ++p) *p = tolower(*p);
}

// Utility function to duplicate strings safely
char *strdup_(const char *s) {
    size_t len = strlen(s) + 1;
    char *dup = (char *)malloc(len);
    if (dup) {
        memcpy(dup, s, len);
    }
    return dup;
}

#define MAX_FIELDS 8       // Maximum number of fields in the TSV line
#define FIELD_BUFFER 256   // Buffer size for individual fields

// Function to split a line into fields based on tab delimiters
void split_line(const char *line, char fields[MAX_FIELDS][FIELD_BUFFER], int *field_count) {
    char *token;
    char buffer[strlen(line) + 1];
    strcpy(buffer, line);

    *field_count = 0;
    token = strtok(buffer, "\t");
    while (token != NULL && *field_count < MAX_FIELDS) {
        strncpy(fields[(*field_count)++], token, FIELD_BUFFER - 1);
        fields[*field_count - 1][FIELD_BUFFER - 1] = '\0'; // Ensure null termination
        token = strtok(NULL, "\t");
    }
}

// Function to parse a line and create a TitleLanguages struct
TitleLanguages *parse_title_languages(const char *line) {
    char fields[MAX_FIELDS][FIELD_BUFFER];
    int field_count = 0;
    split_line(line, fields, &field_count);

    if (field_count < 8) {
        fprintf(stderr, "Error: Invalid line format\n");
        return NULL;
    }

    TitleLanguages *titleLang = malloc(sizeof(TitleLanguages));
    if (!titleLang) {
        fprintf(stderr, "Error: Memory allocation failed\n");
        return NULL;
    }

    // Parse titleId
    strcpy(titleLang->titleId, fields[0]);

    // Parse language (handle \N for missing values)
    titleLang->language = strdup_(fields[4]);
    trim_whitespace(titleLang->language);

    // Parse region (handle \N for missing values)
    titleLang->region = strdup_(fields[3]);
    trim_whitespace(titleLang->region);

    // Parse title (handle \N for missing values)
    titleLang->title = strdup_(fields[2]);
    trim_whitespace(titleLang->title);

    // Parse isOriginalTitle
    titleLang->isOriginalTitle = atoi(fields[7]);

    return titleLang;
}

// Function to load languages, regions, and titles from the file
int load_languages_and_regions(TitleEntry *titles, int title_count) {
    if (title_count <= 0) {
        fprintf(stderr, "Error: Invalid title count\n");
        return -1;
    }

    const char *akas_filename = "imdb/title.akas.tsv";
    FILE *akas_file = fopen(akas_filename, "r");
    if (!akas_file) {
        perror("Failed to open title.akas.tsv file");
        return -1;
    }

    char line[2048];

    // Skip the header line in the akas file
    if (fgets(line, sizeof(line), akas_file) == NULL) {
        fprintf(stderr, "Error: Empty file or missing header\n");
        fclose(akas_file);
        return -1;
    }

    int tc = 0, c = 0; // Title counter
    while (fgets(line, sizeof(line), akas_file)) {
        TitleLanguages *titleLang = parse_title_languages(line);
        if (!titleLang) {
            fprintf(stderr, "Error parsing line: %s", line);
            continue;
        }

        // Find matching title
        while (tc < title_count && strcmp(titles[tc].tconst, titleLang->titleId) < 0) {
            tc++, c++;
        }

        if (c >= title_count) {
            if (c == 0)
                free(titleLang);
            c = 0;
            continue;
        }


        TitleEntry *currentTitle = &(titles[tc]);
        if (!currentTitle) {
            fprintf(stderr, "Error: Null TitleEntry at index %d\n", tc);
            free(titleLang);
            break;
        }

        // Allocate or reallocate memory for languages
        if (currentTitle->languageCount == 0) {
            currentTitle->languages = malloc(sizeof(TitleLanguages) * 2);
            if (!currentTitle->languages) {
                fprintf(stderr, "Error: Memory allocation failed\n");
                free(titleLang);
                fclose(akas_file);
                return -1;
            }
            currentTitle->languageCount = 0;
            currentTitle->languageCount_max = 2;
        } else if (currentTitle->languageCount >= currentTitle->languageCount_max) {
            currentTitle->languageCount_max *= 2;
            TitleLanguages *new_langs = realloc(currentTitle->languages, sizeof(TitleLanguages) * currentTitle->languageCount_max);
            if (!new_langs) {
                fprintf(stderr, "Error: Memory reallocation failed\n");
                free(titleLang);
                fclose(akas_file);
                return -1;
            }
            currentTitle->languages = new_langs;
        }

        // Add the new language entry
        currentTitle->languages[currentTitle->languageCount++] = *titleLang;

        free(titleLang);

    }

    fclose(akas_file);
    return 0;
}


// Load titles from TSV file

// Load titles from TSV file
int load_titles(TitleEntry **titles, int *title_count, RatingInfo* ratings, int ratings_count) {
    const char *filename = "imdb/title.basics.tsv";
    FILE *file = fopen(filename, "r");
    if (!file) {
        perror("Failed to open file");
        return -1;
    }

    char line[1024];
    int count = 0, capacity = 100;
    *titles = malloc(capacity * sizeof(TitleEntry));

    // Skip the header line
    fgets(line, sizeof(line), file);

    while (fgets(line, sizeof(line), file)) {
        if (count >= capacity) {
            capacity *= 2;
            *titles = realloc(*titles, capacity * sizeof(TitleEntry));
        }

        TitleEntry *entry = &(*titles)[count];
        char *token = strtok(line, "\t");
        strcpy(entry->tconst, token);
        entry->tconst[12] = '\0';

        token = strtok(NULL, "\t");
        strcpy(entry->titleType, token);

        token = strtok(NULL, "\t");
        strcpy(entry->primaryTitle, token);

        token = strtok(NULL, "\t");
        strcpy(entry->originalTitle, token);

        token = strtok(NULL, "\t");
        entry->isAdult = atoi(token);

        token = strtok(NULL, "\t");
        entry->startYear = atoi(token);

        token = strtok(NULL, "\t");
        entry->endYear = atoi(token);

        token = strtok(NULL, "\t");
        strcpy(entry->runtimeMinutesgenres, token ? token : "");

        entry->languageCount = 0;
        entry->languageCount_max = 0;

        entry->ratingInfo = get_rating_by_tconst(entry->tconst, ratings, &ratings_count);
        count++;
    }

    fclose(file);
    *title_count = count;
    return 0;
}

// Comparison function for qsort
int compare_results(const void *a, const void *b) {
    SearchResult *result_a = (SearchResult *)a;
    SearchResult *result_b = (SearchResult *)b;
    return result_b->score - result_a->score;  // Sort in descending order of score
}
// Macro for calculating word value based on index
#define CALCULATE_WORD_VALUE(index) ((index <= 3) ? 100 : (index > 12 ? 5 : 100 - (95 * (index - 3) / 9)))
int string_difference_percentage(const char *query, const char *title) {
    int totalScore = 0;
    int wordCount = 0; // Count of query words
    int matchCount = 0; // Count of matched words
    const char *wordStart = query;
    const char *wordEnd = query;
    
    // Go through each word in the query
    while (*wordEnd != '\0') {
        // Skip to the end of the current word
        while (*wordEnd != ' ' && *wordEnd != '\0') {
            wordEnd++;
        }

        // Create a temporary substring for the word
        size_t wordLength = wordEnd - wordStart;
        char tempWord[wordLength + 1];
        strncpy(tempWord, wordStart, wordLength);
        tempWord[wordLength] = '\0'; // Null terminate the word

        // Look for the word in the title
        if (strstr(title, tempWord)) {
            matchCount++;
            totalScore += CALCULATE_WORD_VALUE(matchCount);
        }

        // Move to the next word
        if (*wordEnd != '\0') {
            wordEnd++; // Skip the space
        }
        wordStart = wordEnd;
        wordCount++;
    }

    // Normalize the score: proportional to query words
    return wordCount > 0 ? (totalScore / wordCount) : 0;
}

float string_difference_percentage_(const char *query, const char *str2) {
    int len1 = strlen(query);  // Length of the query string
    int len2 = strlen(str2);   // Length of the second string
    int diff_count = 0;

    // Compare character by character up to the length of the query string
    for (int i = 0; i < len1 && i < len2; i++) {
        if (query[i] != str2[i]) {
            diff_count++;
        }
    }

    // Add the remaining characters from the longer string as differences (only for str2)
    diff_count += abs(len1 - len2);

    // Avoid division by zero (if the query string is empty)
    if (len1 == 0) {
        return 100.0;
    }

    // Calculate the similarity percentage based on the query string's length
    float similarity = 100.0 - ((float)diff_count / len1) * 100;

    // Ensure that similarity percentage stays within the range [0, 100]
    if (similarity < 0.0) {
        similarity = 1.0;  // No similarity if completely different
    } else if (similarity > 100.0) {
        similarity = 100.0;  // Full similarity if identical
    }


    return similarity;
}

// Function to calculate the score based on the query and title
float calculate_score_single(const char *query, char* title, float rank) {
    float score = 0;
    char title_copy[66], query_copy[66];// str_part[156] = "\0";
    // Words that trigger an early break if their index is greater than 3
    // const char *break_words[] = {"by", "|", NULL};

    // Copy and convert both title and query to lowercase for case-insensitive matching
    strncpy(title_copy, title, 65);
    strncpy(query_copy, query, 65);
    to_lowercase(title_copy);
    to_lowercase(query_copy);


    score = (string_difference_percentage(query_copy, title_copy) +
             string_difference_percentage_(query_copy, title_copy) ) / 2;
    //if (score == 100) 
    score += rank;
    return score;
}

float calculate_score(const char *query, TitleEntry* entry, float rank, int* selected_id, char* selected) {
    float score = calculate_score_single(query, entry->originalTitle, rank);

    float score_n = 0;

    for (int i = 0; i < entry->languageCount; i++) {
        if (entry->languages == NULL) break;
        if (entry->languages[i].title == NULL) continue;

        // Check if the title is valid based on the given criteria
        if (entry->languages[i].isOriginalTitle ||
            (entry->languages[i].region != NULL && strcmp(entry->languages[i].region, "DE") == 0) ||
            (entry->languages[i].language != NULL && strcmp(entry->languages[i].language, "de") == 0)) {

            score_n = calculate_score_single(query, entry->languages[i].title, rank);

            if (score_n > score) {
                score = score_n;
                *selected_id = i;

                // Ensure selected is always assigned
                if (entry->languages[i].isOriginalTitle) {
                    strcpy(selected, "original");
                } else if (entry->languages[i].language != NULL && strcmp(entry->languages[i].language, "de") == 0) {
                    strcpy(selected, "DE");
                } else {
                    strcpy(selected, "de");
                }
            }
        }

    }
    return score;
}


#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

// Structure for passing data to each thread
typedef struct {
    const char *query;
    TitleEntry *titles;
    int start_idx;
    int end_idx;
    SearchResult *temp_results;
    int *result_count;
    int thread_id;
    int num_threads;
    float rank;
} ThreadData;

#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

pthread_mutex_t result_mutex = PTHREAD_MUTEX_INITIALIZER;

// Thread function to process a portion of titles
void *search_titles_thread(void *arg) {
    ThreadData *data = (ThreadData *)arg;
    int local_result_count = 0;

    for (int i = data->start_idx; i < data->end_idx; i++) {
        float score = calculate_score(data->query, &data->titles[i], 
                                      data->titles[i].ratingInfo ? data->titles[i].ratingInfo->averageRating : 0, 
                                      &data->temp_results[local_result_count].selected_id, 
                                      data->temp_results[local_result_count].selected);

        // Store the result
        strncpy(data->temp_results[local_result_count].tconst, data->titles[i].tconst, sizeof(data->temp_results[local_result_count].tconst) - 1);
        data->temp_results[local_result_count].tconst[sizeof(data->temp_results[local_result_count].tconst) - 1] = '\0'; // Ensure null termination

        strncpy(data->temp_results[local_result_count].primaryTitle, data->titles[i].primaryTitle, sizeof(data->temp_results[local_result_count].primaryTitle) - 1);
        data->temp_results[local_result_count].primaryTitle[sizeof(data->temp_results[local_result_count].primaryTitle) - 1] = '\0'; // Ensure null termination

        data->temp_results[local_result_count].title = &data->titles[i];
        data->temp_results[local_result_count].score = score;
        local_result_count++;
    }

    // Safely update shared result count
    pthread_mutex_lock(&result_mutex);
    *data->result_count += local_result_count;
    pthread_mutex_unlock(&result_mutex);

    pthread_exit(NULL);
}

// Main search function
int search_titles(const char *query, TitleEntry *titles, int title_count, SearchResult *results, int max_results) {
    int num_threads = NUM_THREADS; // Adjust based on available cores
    pthread_t threads[num_threads];
    ThreadData thread_data[num_threads];

    int result_count = 0;
    SearchResult *temp_results = malloc(title_count * sizeof(SearchResult));

    // Divide titles among threads
    int chunk_size = title_count / num_threads;

    for (int i = 0; i < num_threads; i++) {
        thread_data[i].query = query;
        thread_data[i].titles = titles;
        thread_data[i].start_idx = i * chunk_size;
        thread_data[i].end_idx = (i == num_threads - 1) ? title_count : (i + 1) * chunk_size;
        thread_data[i].temp_results = temp_results + (i * chunk_size);
        thread_data[i].result_count = &result_count;
        thread_data[i].thread_id = i;
        thread_data[i].num_threads = num_threads;

        pthread_create(&threads[i], NULL, search_titles_thread, (void *)&thread_data[i]);
    }

    // Wait for threads to finish
    for (int i = 0; i < num_threads; i++) {
        pthread_join(threads[i], NULL);
    }

    // Sort results by score (descending order)
    qsort(temp_results, result_count, sizeof(SearchResult), compare_results);

    // Copy top results
    int limit = result_count < max_results ? result_count : max_results;
    for (int i = 0; i < limit; i++) {
        results[i] = temp_results[i];
    }

    free(temp_results);
    return limit;
}

SearchResult* select_result(SearchResult *results, int found, const char* search_query) {
    int choice;
    int page = 0;
    int just_movie = 0;

    // Create an array to store the filtered movie results
    SearchResult* filtered_results[found];
    int filtered_found = 0;

    while (1) {
        // Filter results if just_movie is enabled
        if (just_movie) {
            filtered_found = 0;
            for (int i = 0; i < found; i++) {
                if (strcmp(results[i].title->titleType, "movie") == 0) {
                    filtered_results[filtered_found] = &results[i]; // Store movie result address
                    filtered_found++;
                }
            }
        } else {
            // If not filtering for movies, show all results
            filtered_found = found;
            for (int i = 0; i < found; i++) {
                filtered_results[i] = &results[i]; // Store all result addresses
            }
        }

        int page_size = PAGE_SIZE;
        int pages = (filtered_found + page_size - 1) / page_size; // Calculate number of pages

        printf(MAGENTA "\n┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" RESET);
        printf(MAGENTA "┃ " WHITE "%-24s" CYAN " (%d/%d)\n" RESET, search_query, page + 1, pages);
        printf(MAGENTA "┠━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" RESET);

        // Display results for the current page
        int displayed = 0;
        for (int i = page * page_size; i < (page + 1) * page_size && displayed < page_size && i < filtered_found; i++) {
            SearchResult *result = filtered_results[i];

            // Display basic result info with colors
            printf(MAGENTA "┃" GREEN "%2d.%s %s%-24s%s %s(Score: %.2f)%s\n", 
                   i + 1, RESET, 
                   YELLOW, result->primaryTitle, RESET, 
                   CYAN, result->score, RESET);
            if (strlen(result->selected) > 0)
                printf(MAGENTA "┃" RESET "   %s: " WHITE "%-24s\n" RESET, result->selected, result->title->languages[result->selected_id].title);

            // Get rating and vote count
            float r = result->title->ratingInfo != NULL ? result->title->ratingInfo->averageRating : 0;
            int rc = result->title->ratingInfo != NULL ? result->title->ratingInfo->numVotes : 0;

            // Display detailed information with color
            printf(MAGENTA "┃" RESET "  Type: " WHITE "%s, " RESET "Rank: " CYAN "%.2f, " RESET "Rank_c: " CYAN "%d   " YELLOW "%s\n" RESET, result->title->titleType, r, rc, result->selected);

            // Display year, ID, and genres with colors
            printf(MAGENTA "┃" RESET "  Year: " WHITE "%d-%d, " RESET "ID: " WHITE "%s, " RESET "Genres: " WHITE "%s\n" RESET, 
                   result->title->startYear, result->title->endYear,
                   result->title->tconst,
                   result->title->runtimeMinutesgenres);

            fflush(stdout);
            displayed++;
        }

        printf(MAGENTA "┠──────────────────────────────────────────────\n" RESET);
        printf(MAGENTA "┃ " WHITE "%-24s" CYAN " (%d/%d)\n" RESET, search_query, page + 1, pages);
        printf(MAGENTA "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" RESET);

        printf("\nSelect (%s1-%d%s), [n|p] next/prev page, [q|movie]:\n", YELLOW, filtered_found, RESET);
        char input[10];
        fgets(input, sizeof(input), stdin);
        trim_whitespace(input);

        if (input[0] == 'q') {
            // Return an empty result or handle quit as needed
            return NULL;
        } else if (strcmp(input, "movie") == 0) {
            just_movie = !just_movie;
        } else if (input[0] == 'n' && page < pages - 1) {
            page++;  // Go to the next page
        } else if (input[0] == 'p' && page > 0) {
            page--;  // Go to the previous page
        } else {
            // Ensure we are reading a valid number and not an out-of-bounds selection
            if (sscanf(input, "%d", &choice) == 1 && choice >= 1 && choice <= filtered_found) {
                return filtered_results[choice - 1];  // Return the selected result
            } else {
                printf("Invalid selection, try again.\n");
            }
        }
    }
}


// Function to search and select a title based on a query
SearchResult* search_and_select(const char *query, TitleEntry *titles, int title_count) {
    printf("\n\n\n–––––––––––––––––––––––––––––––––––––––––––––––––––––––\n");
    printf("Search and select\n");
    printf("Title: %s\n", query);
    SearchResult results[1000]; // Increase size if needed
    SearchResult* selected = NULL;
    int found = search_titles(query, titles, title_count, results, 1000);

    printf("Found %d results:\n", found);

    if (found > 0) {
        printf("########################################\n");
        printf("### Results for: %s\n", query);

        // Call select_result to allow the user to select a result
        selected = select_result(results, found, query);
        if (selected == NULL)
            return NULL;

        // Check if the result is empty or the user chose to quit
        if (selected->score == 0 && selected->primaryTitle[0] == '\0') {
            printf("\nNo selection was made.\n");
        }

        // Print the selected result
        printf("\nYou selected: %s (Score: %.2f)\n", selected->title->primaryTitle, selected->score);
        printf("Year: %d, Genres: %s\n", selected->title->startYear, selected->title->runtimeMinutesgenres);
    } else {
        printf("No results found.\n");
    }
    return selected; // Exit the function early
}